10

【前端重温系列】this的不可描述与变化原理(献给2020-1024的礼物)

 3 years ago
source link: https://blog.vadxq.com/article/frontend-revisited-this/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【前端重温系列】this的不可描述与变化原理(献给2020-1024的礼物)

时间已经是2020年了,马上也就2021年了,现如今的发展,ES6的实现几乎现代化浏览器都实现了。红宝书也出第四版了,删除了过旧的知识,引入了ES6,涵盖至ECMAScript 2019新标准。新的基础工具书的推出,自己也该好好重温一遍基础,重新梳理自己脑海的知识点,过时的删除,腾出空间给新知识。当然,内容主要还是从核心知识开始,扩展性涵盖其他知识点。

今天是本次前端重温系列的第二篇,有关于this的指向原则和call/apply/bind等原理。欢迎各位看官收看。

js的内存管理和堆栈

谈到this,就不得不说一说javascript的内存管理机制。

有许多语言会暴露内存管理的api给开发者,也有的语言会自己默默的独自完成内存的管理操作。

内存生命周期

不管什么程序语言,内存生命周期基本是一致的:

  • 分配你所需要的内存
    • 值的初始化:js在定义变量时就完成了内存分配
    • 通过函数调用分配内存
  • 使用分配到的内存(读、写)
  • 不需要时将其释放\归还

栈内存与堆内存

js有两大数据类型:基本类型和引用类型

基本类型往往在栈内存里存储,而引用类型往往在堆内存里存储。

堆和栈分别是不同的数据结构。栈是线性表的一种,而堆则是树形结构。

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

  • 引用计数垃圾收集

    • 这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
  • 标记-清除算法

    • 这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象。从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

内存泄漏的概念

该释放的变量(内存垃圾)没有被释放,仍然霸占着原有的内存不松手,导致内存占用不断攀高,带来性能恶化、系统崩溃等一系列问题,这种现象就叫内存泄漏。

var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // 'originalThing'的引用
console.log("嘿嘿嘿");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("哈哈哈");
}
};
};
setInterval(replaceThing, 1000);

这段代码有什么问题吗?

在 V8 中,一旦不同的作用域位于同一个父级作用域下,那么它们会共享这个父级作用域。unused 是一个不会被使用的闭包,但和它共享同一个父级作用域的 someMethod,则是一个 “可抵达”(也就意味着可以被使用)的闭包。unused 引用了 originalThing,这导致和它共享作用域的 someMethod 也间接地引用了 originalThing。结果就是 someMethod “被迫” 产生了对 originalThing 的持续引用,originalThing 虽然没有任何意义和作用,却永远不会被回收。不仅如此,originalThing 每次 setInterval 都会改变一次指向(指向最近一次的 theThing 赋值结果),这导致无法被回收的无用 originalThing 越堆积越多,最终导致严重的内存泄漏。

可能导致内存泄漏的写法

  • 无意义的全局变量
function a() {
b = 0
}
  • 未清除的setInterval和链式调用的setTimeout
setInterval(function() {
}, 1000);
setTimeout(function() {
setTimeout(arguments.callee, 1000);
}, 1000);
  • 清除不当的变量
var myDiv = document.getElementById('myDiv')

function handleMyDiv() {
// 一些与myDiv相关的逻辑
}

// 使用myDiv
handleMyDiv()

// 尝试删除,但是由于前面函数引用了,在内存上的表现还是可访问的地址,也就是没删除掉。
document.body.removeChild(document.getElementById('myDiv'));

this的指向原则

this指向:指向执行时所在的上下文,即被调用函数所在的对象

  • this的指向由函数执行时确定,而不是定义时决定的。这点和闭包恰恰相反。当调用方法没有明确对象时,则是指向window

  • 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

var o = {
a:10,
b:{
a:12,
fn: function(){
console.log(this.a); // 12
}
}
}
o.b.fn();
  • this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的

  • 如果 new 关键词出现在被调用函数的前面,那么JavaScript引擎会创建一个新的对象,被调用函数中的this指向的就是这个新创建的函数。

  • 如果通过apply、call或者bind的方式触发函数,那么函数中的this指向传入函数的第一个参数

  • 如果一个函数是某个对象的方法,并且对象使用句点符号触发函数,那么this指向的就是该函数作为那个对象的属性的对象,也就是,this指向句点左边的对象。

this特殊情形

  • this必然指向window的情况

    • 立即执行函数(IIFE)
    • setTimeout 中传入的函数
    • setInterval 中传入的函数
  • 严格模式的情形

    • 严格模式下,this 将保持它被指定的那个对象的值,所以,如果没有指定对象,this 就是 undefined
    • 箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定
  • 如果返回值是一个Object,那么this指向的就是那个返回的对象,否则指向函数的实例

// null
function fn()
{
this.user = 'vadxq';
return null;
}
var a = new fn;
console.log(a.user); // vadxq

// return fn
function fn()
{
this.user = 'vadxq';
return function() {};
}
var a = new fn;
console.log(a.user); // undefined


// return Object
function fn()
{
this.user = 'vadxq';
return {};
}
var a = new fn;
console.log(a.user); // undefined

// return other
function fn()
{
this.user = 'vadxq';
return undefined;
}
var a = new fn;
console.log(a.user); // vadxq

改变this

改变this的方法途径

  • 书写定义时改变,比如箭头函数
  • 调用时改变,显式地调用一些方法,比如call/apply/bind

箭头函数是在定义的时候就决定了指向

var a = 1

var obj = {
a: 2,
// 声明位置
showA: () => {
console.log(this.a)
}
}

// 调用位置
obj.showA() // 1

构造函数:构造函数里面的 this 会绑定到我们 new 出来的这个对象上

call/apply/bind的特点

  • call
    • 改变后直接调用
    • fn.call(ctx, arg1, arg2)
  • apply
    • 改变后直接调用
    • fn.apply(ctx, [arg1, arg2])
  • bind
    • 改变后不进行调用操作
    • fn.bind(ctx, arg1, arg2)

实现call/apply/bind方法

可以看此文章,写的很详细:

手写call、apply、bind实现及详解

这一篇文章由于内容涵盖了的知识比较的偏底层和js语法的特性,在准备花费的时间较长,由于这个阶段自己正好在寻找工作,断断续续的在填坑,最后的内容是在去入职的火车上完成的哈哈哈。算是比较有意义的一个纪念!特写了个后记记录一下。

同时今天又是1024!我们的狂欢🎉!献给2020-1024的礼物!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK