前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解this绑定

深入理解this绑定

作者头像
Chor
发布2019-11-07 19:21:42
4690
发布2019-11-07 19:21:42
举报
文章被收录于专栏:前端之旅

1.调用位置

js中的词法作用域是静态的,需要关注的往往是函数的声明位置而不是调用位置—–例如闭包引用自由变量时,应该注意闭包函数的声明位置;而this却在某种程度上类似于动态作用域,this到底绑定的是谁,要看函数的调用位置(或者说调用方法),只有在函数调用的时候this的指向才能被确定。

确定当前执行函数的调用位置,有两种方法:

1.1 分析调用栈

调用栈即:为了到达当前执行位置所调用的所有函数。而当前执行函数的调用位置就在该函数的前一个调用中。

代码语言:javascript
复制
function baz() {
    //当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log("baz");
    bar(); // bar的调用位置
}
function bar() {
    // 当前调用栈是baz -> bar
    // 因此,当前调用位置在baz中
    console.log("bar");
    foo(); // foo的调用位置
}
function foo() {
    // 当前调用栈是baz -> bar -> foo
    // 因此,当前调用位置在bar中
    console.log("foo");
}
baz(); // <-- baz的调用位置

如上代码,例如当前执行函数为bar,bar函数的调用位置即bar函数的前一个调用,分析调用栈baz -> bar可知,是baz。

1.2 设置断点或debugger

上面的方法将调用栈当作了函数调用链,这种方法比较麻烦,且容易出错,所以我们采取设置断点或debugger的方法寻找调用位置。我们在上面代码的foo函数中的第一行插入debugger;,那么运行代码时(当前执行函数是foo),调试器会在那个位置暂停,右侧的call stack展示了当前位置的函数调用列表,即调用栈。而调用位置就是栈中的第二个元素。

2.绑定规则

2.1 默认绑定

可以把默认绑定看作是无法应用其他规则时的默认规则,this指向全局对象。独立函数调用(如代码中的foo函数,它是直接使用不带任何修饰的函数引用进行调用的)应用的就是默认绑定规则。

代码语言:javascript
复制
function foo(){
    console.log(this.a);
}
var a = 2;
foo();   //2

但是,函数运行在严格模式下时,this的默认绑定将无法绑定全局对象,而是绑定到undefined。

代码语言:javascript
复制
function foo() {    
    "use strict";   // 函数运行在严格模式下
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

同时,函数在严格模式下调用时,默认绑定不受影响。

代码语言:javascript
复制
function foo() { 
    console.log( this.a );
}
var a = 2;
(function() { 
    "use strict";  //函数在严格模式下调用    
    foo(); // 2
})();

2.2 隐式绑定

当函数引用有上下文对象时(或者说被某个对象“包含”/“拥有”),隐式绑定规则会把函数中的this绑定到这个上下文对象。

代码语言:javascript
复制
function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

对象属性引用链中只有上一层或者说最后一层在调用中起作用。

代码语言:javascript
复制
function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    obj2: obj2
};
var obj2={
    a: 42
    foo: foo
}
obj1.obj2.foo(); // 42
2.2.1 隐式丢失

隐式绑定在一些情况下会丢失绑定对象,应用默认绑定,使this指向全局对象或者undefined。以下情况会发生隐式丢失:

  • 将绑定上下文对象的函数的引用赋值给变量并调用
代码语言:javascript
复制
function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo: foo
}
var a = 3;
var bar = obj.foo;
bar();

虽然bar是obj.foo的一个引用,但实际上是直接引用了foo函数本身,此时的bar()是不带任何修饰的函数调用,因此使用了默认绑定

  • 传入回调函数
代码语言:javascript
复制
function foo(){
    console.log(this.a);
}
function bar(fn){
    fn();
}
var obj = {
    a:2,
    foo: foo
}
var a = 3;
bar(obj.foo);

传参其实是隐式赋值,即把实参(这里是绑定上下文对象的函数的引用)赋值给形参变量,该变量也是直接引用了foo函数本身,和上面的情况其实是一样的。

这也解释了为什么传参给setTimeout函数时会发生隐式丢失:

代码语言:javascript
复制
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var a = 3;
setTimeout(obj.foo, 100); // 3

因为上面的代码实际上相当于:

代码语言:javascript
复制
(function setTimeout(fn,100){
    // 100......
    fn();
})(obj.foo);

2.3 显式绑定

2.3.1 call()apply()

call() 或者 apply() 方法接受一个 thisArg,将函数的 this 绑定到该 thisArg。 thisArg 的取值有以下四种情况:

  • 不传,或者传null,undefined:函数中的 this 指向 window 对象
  • 传递另一个函数的函数名:函数中的 this 指向这个函数的引用
  • 传递字符串、数值或布尔类型等基本类型:函数中的 this 指向其对应的包装对象,如 String、Number、Boolean
  • 传递一个对象:函数中的 this 指向这个对象
代码语言:javascript
复制
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
foo.call(obj); // 2
2.3.2 硬绑定 bind()

但是这两种方法依然无法解决绑定丢失的问题,所以有了硬绑定:

代码语言:javascript
复制
function foo() {
  console.log( this.a);
}
var obj = {
  a: 2
};
var bar = function() {
  foo.call(obj);
};
bar();  // 2
setTimeout(bar, 100);  // 2

bar.call(window);  //无效,硬绑定之后的this不可再更改

硬绑定新创建了一个函数,并在该函数内部完成了this的绑定,之后不管怎么调用新建的这个函数,this的绑定都不会丢失。

典型应用场景是 1.创建一个包裹函数,负责接收参数并返回值。

代码语言:javascript
复制
function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = function() {
    return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

2.创建一个可以重复使用的辅助函数。

代码语言:javascript
复制
function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}

// 简单的辅助绑定函数
function bind(fn, obj) {
    return function() {
        return fn.apply( obj, arguments );
    }
}
var obj = {
    a: 2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

ES5提供了内置方法Function.prototype.bind,对上面这种辅助函数进行了封装:

代码语言:javascript
复制
function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

bind()方法将返回一个完成硬绑定的新函数。

2.3.3 API调用的“上下文”

同样可以解决绑定丢失的问题。 JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)一样,确保回调函数使用指定的this。这些函数实际上通过call(..)和apply(..)实现了显式绑定。

代码语言:javascript
复制
var obj = {
    id: "awesome"
}
var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( function foo(el) {
    console.log( el, this.id );
}, obj );
// 1 awesome 2 awesome 3 awesome

2.4 new 绑定

这篇文章中,其实已经谈到了new的内部原理,在这里再做一下总结—–使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

  • 1.创建一个新对象
  • 2.为该对象执行[[prototype]]链接
  • 3.将该对象绑定到构造函数的this
  • 4.如果函数没有显式返回对象,则new操作最终将返回步骤1中创建的新对象

基于这些步骤,我们就可以手写实现new了,具体过程依然可以参考上面链接的文章。

有时候会将硬绑定与new一起使用,目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数(柯里化

代码语言:javascript
复制
function foo(p1, p2) {
    this.val = p1 + p2;
}

// 之所以使用null是因为在本例中我们并不关心硬绑定的this是什么
// 反正使用new时this会被修改
var bar = foo.bind( null, "p1" );

var baz = new bar( "p2" );

baz.val; // p1p2

3. this的判断

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条this绑定规则。可以按照下面的顺序来进行判断:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定 到全局对象。 var bar = foo()

4.绑定例外

4.1 被忽略的this

null或者undefined作为this的绑定对象传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认规则。

下面两种情况下会传入null:

  • 使用apply(..)来“展开”一个数组,并当作参数传入一个函数
  • bind(..)可以对参数进行柯里化(预先设置一些参数)
代码语言:javascript
复制
function foo(a, b) {
    console.log( "a:" + a + ",b:" + b );
}
// 把数组”展开“成参数
foo.apply( null, [2, 3] ); // a:2,b:3

// 使用bind(..)进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2,b:3 

总是传入null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。

更安全的做法:

传入一个空对象,把this绑定到这个对象不会对你的程序产生任何副作用。

代码语言:javascript
复制
function foo(a, b) {
    console.log( "a:" + a + ",b:" + b );
}

// 我们的空对象
var ø = Object.create( null );

// 把数组”展开“成参数
foo.apply( ø, [2, 3] ); // a:2,b:3

// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3 

4.2 间接引用

你可能会有意无意地创建一个函数的间接引用,尤其是在赋值的时候

代码语言:javascript
复制
// p.foo = o.foo的返回值是目标函数的引用,所以调用位置是foo()而不是p.foo()或者o.foo()
function foo() {
    console.log( this.a );
}

var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};

o.foo(); // 3
(p.foo = o.foo)(); // 2

4.3 软绑定

  • 硬绑定可以把this强制绑定到指定的对象(new除外),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。
  • 如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
代码语言:javascript
复制
// 默认绑定规则,优先级排最后
// 如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this
if(!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕获所有curried参数
        var curried = [].slice.call( arguments, 1 ); 
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ? 
                    obj : this,
                curried.concat.apply( curried, arguments )
            );
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}

使用:软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。

代码语言:javascript
复制
function foo() {
    console.log("name:" + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj 

// 隐式绑定规则
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 看!!!

// 显式绑定规则
fooOBJ.call( obj3 ); // name: obj3 <---- 看!!!

// 绑定丢失,应用软绑定
setTimeout( obj2.foo, 10 ); // name: obj

5 this词法

5.1 箭头函数

ES6新增了箭头函数,上述四条规则对这种函数是不生效的。

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。

拿下面的代码举例,箭头函数在词法层面的上一层是foo(),所以它的this和foo()的this是一样的。由于foo()的this绑定到obj1,所以bar(引用箭头函数)的this也会绑定到obj1。需要注意的是,箭头函数的绑定无法被修改 —— 因为箭头函数没有自己的 this,所以是不能对它使用 callapplybind 的,new也不行。

代码语言:javascript
复制
function foo() {
    // 返回一个箭头函数
    return (a) => {
        // this继承自foo()
        console.log( this.a );
    };
}

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
}
// 绑定foo()的this为obj
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2,不是3!

箭头函数常用于回调函数中,例如事件处理器或者定时器。

代码语言:javascript
复制
function foo() {      
    setTimeout(() => { 
          // 这里的this在词法上继承自foo()        
          console.log( this.a );     
     },100); } 

var obj = {     
    a:2
};  
foo.call( obj ); // 2

5.2 self = this与箭头函数

this在通常情况下都是动态作用域的,而箭头函数很明显是静态(词法)作用域。实际上,在ES6之前,也有类似于箭头函数的模式—–self = this,采用的正是词法作用域:

代码语言:javascript
复制
function foo() {
    var self = this; // lexical capture of this
    setTimeout( function() {
        console.log( self.a ); // self只是继承了foo()函数的this绑定
    }, 100 );
}

var obj = {
    a: 2
};

foo.call(obj); // 2

5.3 代码风格统一

如果你经常编写this风格的代码,但是绝大部分时候都会使用self = this或者箭头函数来否定this机制,那你或许应当:

  1. 只使用词法作用域并完全抛弃错误 this 风格的代码;
  2. 完全采用 this 风格,在必要时使用 bind(..),尽量避免使用 self = this 和箭头函数。

当然,包含这两种代码风格的程序可以正常运行,但是在同一个函数或者同一个程序中混合使用这两种风格通常会使代码更难维护,并且可能也会更难编写。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-04-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.调用位置
    • 1.1 分析调用栈
      • 1.2 设置断点或debugger
      • 2.绑定规则
        • 2.1 默认绑定
          • 2.2 隐式绑定
            • 2.2.1 隐式丢失
          • 2.3 显式绑定
            • 2.3.1 call() 和 apply()
            • 2.3.2 硬绑定 bind()
            • 2.3.3 API调用的“上下文”
          • 2.4 new 绑定
          • 3. this的判断
          • 4.绑定例外
            • 4.1 被忽略的this
              • 4.2 间接引用
              • 4.3 软绑定
              • 5 this词法
                • 5.1 箭头函数
                  • 5.2 self = this与箭头函数
                    • 5.3 代码风格统一
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档