是时候看清JS 中的 this 了

this 到底是个什么东西

MDN 上对于 this 如此定义:

A property of an execution context (global, function or eval) that, in non–strict mode, is always a reference to an object and in strict mode can be any value.

上面这段话,主要讲的是 this 的存在形式,这当然不太够,也许我们更想要搞明白的是,它是为了什么存在。

事实上,当我们在获取一个变量的时候,我们想要知道,它是谁的?当我们执行一个函数的时候,我们又得知道,这一次函数的执行,到底是针对谁的?

我想,这就是 this。

如何确定 this? call, bind, apply

它在非严格模式下总是一个对象,在严格模式下,可以是任何值。

当我们没有特别指定 this 的时候,this 实际上就是它所出现在的执行环境所对应的对象,我们在 window 中写个 this,代码跑到这里的时候,编译器就知道,这个 this 就是指 window.

而如果我们想要(需要)主动设置 this …… 那,就要深入谈一谈 call / bind / apply 了

这三个方法主要是用于函数的,当我们调用一个函数的时候,这个函数的这一次执行,究竟是“针对谁”,这是每个函数在每一次执行的时候都要考虑的。我们所习以为常的 fn() 实际上是一个语法糖,当我们在 window 里面执行这个函数的时候,它的原始面貌是:
fn.call(window)

call / bind / apply 的第一参数就的 this.

也就是说,如果我们不偷懒的话, fn.call 才是真正的“函数调用”(即,调用了函数的 call 方法),我们每次调用函数的时候都要告诉函数 this 是什么。这里,我们只比较 call 与 bind,因为,apply 与 call 很类似,只是参数的形式不一样。
bind 是什么呢?当 bind 出现的时候,实际上表达的意思是 “我要重新创一个看起来一样的函数,但是这个函数执行的时候 this 永远指向我最初指定的。”
所以,fn.bind(window) 得到,不是函数的执行,而是一个 this 被“绑”死了的函数。那么,它与 call 的差异显而易见: call 是执行函数,bind 是创建函数,就算是 bind 生成的函数在执行的时候,依然需要用到 call 方法,只不过,call 指定的 this 是无效的。

let b = fn.bind(window)
b.call(a) // 实际还是 b.call(window),因为 b 变量对应的函数,在创建的时候 this 已经 bind.

这一点有什么用呢?举个例子:
MDN 中对于数组的 slice 方法有一个描述,

slice method can also be called to convert Array-like objects / collections to a new Array.
slice 方法可以接收一个类数组,返回一个新数组。

于是我们就可以构建一个“数组转换器”:

let slice = Function.prototype.call.bind(Array.prototype.slice)
// 这是 MDN 中的例子

你没看错,我们在 call 后面在接一个 bind,因为 call 也是一个函数,
这段代码翻译成人话就是,slice 这个变量,被赋值了一个 call 函数,这个 call 函数的 this 绑定了 Array.prototype.slice
(有意思,不只是普通函数有 this,call 函数也有 this 的)
于是,我们就可以用了:

slice({0:"a", 1:"b", length:2}) // → ["a", "b"]

好处是,上面这段代码就等价于:

Array.prototype.slice({0:"a", 1:"b", length:2})
//为了对比,我们采用不用 call 的语法糖写法

你看,这一下子就省了1… 2… 3 …… 好多好多字符呢~

总结下来,每当我们执行一个函数(非箭头函数)的时候,只要看一看这个函数相关的 bind,再把它转化为 fn.call(this) 的形式就能够很好确定 this 了。

对了,语法糖写法的 this 就是被调用的函数前面的一大堆东西:

a.b.c.fn() // 等价于 a.b.c.fn.call(a.b.c)

用 class 生成对象时的 this

官方说了算:this 指向新生成的对象

arrow funcion

官方说,箭头函数里面的 this 由箭头函数执行时的环境 this 决定,换而言之,箭头函数出不出现,并不影响 this 的指向,我们不需要告诉箭头函数它是针对谁,反正它很“随遇而安” ……

他们说了算 ……

其它

是的,总是“他们说了算”,当我们调用任何一个 API 的时候,实际的代码对于我们来说就好像是藏在“黑匣子”里,this 是什么,只有开发人员知道:

dom.onclick = fn

在dom API 的事件触发函数中, this 是触发当前事件的元素。

再比如,React 在调用 onClick 的时候,会强制把 this 指向 undefined …… 所以我们在 React 中做父子通信的时候常常要用到 bind。

严格模式

还是看回最初的那段话:

A property of an execution context (global, function or eval) that, in non–strict mode, is always a reference to an object and in strict mode can be any value.

它是执行环境下的一个属性,在非严格模式下,总是指向对象,在严格模式下,可以是任何值。
非常有意思,在非严格模式下,我们尝试把 this 指向 字符串、数字、布尔值 的时候,浏览器会把这些值转换为对象的形式。
而如果我们尝试把 this 指向 undefined, null 的时候,this 会变成 window.

this1.png

而在严格模式下,我们指定 this 是什么,它就真的是什么

写在最后

在讨论 bind & call 的时候,我们就注意到一个有去的想象:bind / call 本身就是函数,那么他们有没有 call, bind 函数呢?肯定有,因为 javascript 中这些方法都只不过是一个继承

this2.png

就像最初的例子, call.bind 执行,call.call 也存在,之所以 fn.call.call() 会报错,那是因为,默认情况下,使用 call 又没输入 this 参数的时候,this 默认是 window, 而 call 的 this 必须是一个函数(别忘了,函数是一类特殊的对象):

this3.png

其实还有点什么想说

当我在思考 this 的时候,我总不由自主地受到“作用域”的影响。
现在看来,还是挺明了的:this 跟作用域关系不大的,作用域讨论的是函数的固有属性,而在函数里面讨论到 this 是在确定函数某一次执行的特定归属 ……

推荐阅读更多精彩内容