目录
this 是什么
this 的四种绑定规则
绑定规则的优先级
绑定例外
扩展: 箭头函数
this 是什么
理解 this 之前, 先纠正一个观点, this 既不指向函数自身, 也不指函数的词法作用域. 如果仅通过 this 的英文解释, 太容易产生误导了. 它实际是在函数被调用时才发生的绑定, 也就是说 this 具体指向什么, 取决于你是怎么调用的函数.
this 的四种绑定规则
this 的 4 种绑定规则分别是: 默认绑定, 隐式绑定, 显示绑定, new 绑定. 优先级从低到高.
默认绑定
什么叫默认绑定, 即没有其他绑定规则存在时的默认规则. 这也是函数调用中最常用的规则.
来看这段代码:
- function foo() {} console.log( this.a );
- var a = 2;
- foo(); // 打印的是什么?
foo() 打印的结果是 2.
因为 foo()是直接调用的(独立函数调用), 没有应用其他的绑定规则, 这里进行了默认绑定, 将全局对象绑定 this 上, 所以 this.a 就解析成了全局变量中的 a, 即 2.
注意: 在严格模式下(strict mode), 全局对象将无法使用默认绑定, 即执行会报 undefined 的错误
- function foo() {
- "use strict";
- console.log( this.a );
- }
- var a = 2;
- foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
隐式绑定
除了直接对函数进行调用外, 有些情况是, 函数的调用是在某个对象上触发的, 即调用位置上存在上下文对象.
- function foo() {
- console.log( this.a );
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- obj.foo(); // ?
obj.foo() 打印的结果是 3.
这里 foo 函数被当做引用属性, 被添加到 obj 对象上. 这里的调用过程是这样的:
获取 obj.foo 属性 -> 根据引用关系找到 foo 函数, 执行调用
所以这里对 foo 的调用存在上下文对象 obj,this 进行了隐式绑定, 即 this 绑定到了 obj 上, 所以 this.a 被解析成了 obj.a, 即 3.
多层调用链
- function foo() {
- console.log(this.a);
- }
- var a = 2;
- var obj1 = {
- a: 3,
- obj2: obj2
- };
- var obj2 = {
- a: 4,
- foo: foo
- };
- obj1.obj2.foo(); // ?
obj.foo() 打印的结果是 4.
同样, 我们看下函数的调用过程:
先获取 obj1.obj2 -> 通过引用获取到 obj2 对象, 再访问 obj2.foo -> 最后执行 foo 函数调用
这里调用链不只一层, 存在 obj1,obj2 两个对象, 那么隐式绑定具体会绑哪个对象. 这里原则是获取最后一层调用的上下文对象, 即 obj2, 所以结果显然是 4(obj2.a).
隐式丢失(函数别名)
注意: 这里存在一个陷阱, 大家在分析调用过程时, 要特别小心
先看个代码:
- function foo() {
- console.log(this.a);
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- var bar = obj.foo;
- bar(); //?
- <font color="red">bar() 打印的结果是 2.</font>
为什么会这样, obj.foo 赋值给 bar, 那调用 bar()为什么没有触发隐式绑定, 使用的是默认绑定呢.
这里有个概念要理解清楚, obj.foo 是引用属性, 赋值给 bar 的实际上就是 foo 函数(即: bar 指向 foo 本身).
那么, 实际的调用关系是: 通过 bar 找到 foo 函数, 进行调用. 整个调用过程并没有 obj 的参数, 所以是默认绑定, 全局属性 a.
隐式丢失(回调函数)
- function foo() {
- console.log( this.a );
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- setTimeout( obj.foo, 100 ); // ?
- <font color="red">打印的结果是 2.</font>
同样的道理, 虽然参传是 obj.foo, 因为是引用关系, 所以传参实际上传的就是 foo 对象本身的引用. 对于 setTimeout 的调用, 还是 setTimeout -> 获取参数中 foo 的引用参数 -> 执行 foo 函数, 中间没有 obj 的参与. 这里依旧进行的是默认绑定.
显示绑定
相对隐式绑定, this 值在调用过程中会动态变化, 可是我们就想绑定指定的对象, 这时就用到了显示绑定.
显示绑定主要是通过改变对象的 prototype 关联对象, 这里不展开讲. 具体使用上, 可以通过这两个方法 call(...)或 apply(...)来实现(大多数函数及自己创建的函数默认都提供这两个方法).
call 与 apply 是同样的作用, 区别只是其他参数的设置上
- function foo() {
- console.log(this.a);
- }
- var a = 2;
- var obj1 = {
- a: 3,
- };
- var obj2 = {
- a: 4,
- };
- foo.call(obj1); // ?
- foo.call(obj2); // ?
打印的结果是 3, 4.
这里因为显示的申明了要绑定的对象, 所以 this 就被绑定到了 obj 上, 打印的结果自然就是 obj1.a 和 obj2.a.
硬绑定
- function foo() {
- console.log(this.a);
- }
- var a = 2;
- var obj1 = {
- a: 3,
- };
- var obj2 = {
- a: 4,
- };
- var bar = function() {
- foo.call(obj1);
- }
- setTimeout(bar, 100); // 3
- bar.call(obj2); // 这是多少
前面两个 (函数别名, 回调函数) 打印 3, 因为显示绑定了, 没什么问题.
最后一个打印是 3.
这里需要注意下, 虽然 bar 被显示绑定到 obj2 上, 对于 bar,function(){...} 中的 this 确实被绑定到了 obj2, 而 foo 因为通过 foo.call( obj1 )已经显示绑定了 obj1, 所以在 foo 函数内, this 指向的是 obj1, 不会因为 bar 函数内指向 obj2 而改变自身. 所以打印的是 obj1.a(即 3).
new 绑定
js 中的 new 操作符, 和其他语言中 (如 JAVA) 的 new 机制是不一样的. js 中, 它就是一个普通函数调用, 只是被 new 修饰了而已.
使用 new 来调用函数, 会自动执行如下操作:
如果函数没有返回其他对象, 那么 new 表达式中的函数调用会自动返回这个新对象.
从第三点可以看出, this 指向的就是对象本身.
看个代码:
- function foo(a) {
- this.a = a;
- }
- var a = 2;
- var bar1 = new foo(3);
- console.log(bar1.a); // ?
- var bar2 = new foo(4);
- console.log(bar2.a); // ?
最后一个打印是 3, 4.
因为每次调用生成的是全新的对象, 该对象又会自动绑定到 this 上, 所以答案显而易见.
绑定规则优先级
上面也说过, 这里在重复一下. 优先级是这样的, 以按照下面的顺序来进行判断:
数是否在 new 中调用(new 绑定)? 如果是的话 this 绑定的是新创建的对象.
数是否通过 call,apply(显式绑定)或者硬绑定调用? 如果是的话, this 绑定的是 指定的对象.
数是否在某个上下文对象中调用(隐式绑定)? 如果是的话, this 绑定的是那个上下文对象.
果都不是的话, 使用默认绑定. 如果在严格模式下, 就绑定到 undefined, 否则绑定到 全局对象.
var bar = foo()
规则例外
在显示绑定中, 对于 null 和 undefined 的绑定将不会生效.
代码如下:
- function foo() {
- console.log( this.a );
- }
- foo.call( null ); // 2
- foo.call( undefined ); // 2
这种情况主要是用在不关心 this 的具体绑定对象(用来忽略 this), 而传入 null 实际上会进行默认绑定, 导致函数中可能会使用到全局变量, 与预期不符.
所以对于要忽略 this 的情况, 可以传入一个空对象 ø, 该对象通过 Object.create(null)创建. 这里不用 {} 的原因是,ø 是真正意义上的空对象, 它不创建 Object.prototype 委托,{}和普通对象一样, 有原型链委托关系.
1. 这里传 null 的一种具体使用场景是函数柯里化的使用
扩展: 箭头函数
最后, 介绍一下 ES6 中的箭头函数. 通过 "=>" 而不是 function 创建的函数, 叫做箭头函数. 它的 this 绑定取决于外层 (函数或全局) 作用域.
case 1 (正常调用)
普通函数
- function foo(){
- console.log( this.a );
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- obj.foo(); //3
箭头函数
- var foo = () =>{
- console.log(this.a);
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- obj.foo(); //2
- foo.call(obj); //2 , 箭头函数中显示绑定不会生效
- case 2 (函数回调)
普通函数
- function foo() {
- return function() {
- console.log(this.a);
- }
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- var bar = obj.foo();
- bar(); //2
箭头函数
- function foo() {
- return () = >{
- console.log(this.a);
- }
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- var bar = obj.foo();
- bar(); //3
通过上面两个列子, 我们看到箭头函数的 this 绑定 < font color="red">只取决于外层 (函数或全局) 的作用域</font>, 对于前面的 4 种绑定规则是不会生效的. 它也是作为 this 机制的一种替换, 解决之前 this 绑定过程各种规则带来的复杂性.
注意: 对于 ES6 之前, 箭头函数的替换版本是这样的
- // es6
- function foo() {
- return () = >{
- console.log(this.a);
- }
- }
- var a = 2;
- var obj = {
- a: 3,
- foo: foo
- };
- var bar = obj.foo();
- bar(); //3
通过上面两个列子, 我们看到箭头函数的 this 绑定 < font color="red">只取决于外层 (函数或全局) 的作用域</font>, 对于前面的 4 种绑定规则是不会生效的. 它也是作为 this 机制的一种替换, 解决之前 this 绑定过程各种规则带来的复杂性.
注意: 对于 ES6 之前, 箭头函数的替换版本是这样的
- // es6
- function foo(){
- return () => {
- console.log( this.a );
- }
- }
- // es6 之前的替代方法
- function foo(){
- var self = this;
- return () => {
- console.log( self.a );
- }
- }
总结
我们在使用 js 的过程中, 对于 this 的理解往往觉得比较困难, 再调试过程中有时也会出现一些不符合预期的现象. 很多时候, 我们都是通过一些变通的方式 (如: 使用具体对象替换 this) 来规避的问题. 可问题一直存在那儿, 我们没有真正的去理解和解决它.
本文主要参考了你不知道的 JavaScript(上卷), 对 this 到底是什么, 具体怎么绑定的, 有什么例外情况以及 ES6 中的一个优化方向, 来彻底搞清楚我们一直使用的 this 到底是怎么玩的.
来源: https://segmentfault.com/a/1190000014224541