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

【前端重温系列】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垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

内存泄漏

内存泄漏的概念

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 越堆积越多,最终导致严重的内存泄漏。

可能导致内存泄漏的写法

  • 无意义的全局变量
1
2
3
function a() {
b = 0
}
  • 未清除的setInterval和链式调用的setTimeout
1
2
setInterval(function() {
}, 1000);
1
2
3
setTimeout(function() {
setTimeout(arguments.callee, 1000);
}, 1000);
  • 清除不当的变量
1
2
3
4
5
6
7
8
9
10
11
var myDiv = document.getElementById('myDiv')

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

// 使用myDiv
handleMyDiv()

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

this的指向原则

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

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

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

1
2
3
4
5
6
7
8
9
10
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指向的就是那个返回的对象,否则指向函数的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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

箭头函数

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

1
2
3
4
5
6
7
8
9
10
11
12
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的礼物!

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

https://blog.vadxq.com/article/frontend-revisited-this/

作者

vadxq

发布于

2020-10-24

更新于

2020-10-24

许可协议

评论