this 指向小细节 :)

201 阅读6分钟

this 四大绑定原则

默认绑定

默认情况指向调用对象

function foo() {    
    console.log(this.a);
}
var a = 2;
foo(); // 2

声明在全局作用域中的变量(比如 var a = 2)就是全局对 象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币 的两面一样。

接下来我们可以看到当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?因为在本 例中,函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。

那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看 foo() 是如何调用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用,因此只能使用默认绑定,无法应用其他规则。

如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined

function foo() {
    console.log(this.a);
}
var a = 2;
(function () {
    "use strict";
    foo(); // 2
})();

这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只 有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo() 的调用位置无关

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo,
};
obj.foo(); // 2

首先需要注意的是 foo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。

然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥有”或者“包含”它。

无论你如何称呼这个模式,当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的

显式绑定

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
};
foo.call(obj); // 2

JavaScript 中的“所有”函数都有一些有用的特性,可以用来解决这个问题。具体点说,可以使用函数的 call(..) 和 apply(..) 方法。

严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自 己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。

call apply bind

从一脸懵 B 到完全理解

  • call()、apply()、bind() 都是用来重定义 this 这个对象的!

德玛西亚

obj.myFun.call(db);    // 德玛年龄 99
obj.myFun.apply(db);    // 德玛年龄 99
obj.myFun.bind(db)();   // 德玛年龄 99
  • 对比 call 、bind 、 apply 传参情况下

来自哪里

obj.myFun.call(db,'成都','上海');     // 德玛 年龄 99 来自 成都去往上海
obj.myFun.apply(db,['成都','上海']); // 德玛 年龄 99 来自 成都去往上海
obj.myFun.bind(db,'成都','上海')(); // 德玛 年龄 99 来自 成都去往上海
  • 小结

  • call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,

  • 第二个参数差别就来了:

call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,'成都', ... ,'string' )。

apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,['成都', ..., 'string' ])。

bind 除了返回是函数以外,它 的参数和 call 一样。

new 绑定

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。

  2. 这个新对象会被执行原型连接。

  3. 这个新对象会绑定到函数调用的 this。

  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

函数自执行里的 this 指向 window

因为函数执行时,实际是 window 调用了它,也就是 window.函数名();那么,里面的 this 指向当前调用该函数的对象,就是 window。

<input type="button" value="modify" οnClick="changeContent()" />

写在元素上 onclick 里面的函数 changeContent,相当于函数直接调用,函数里面使用的 this 指向全局对象 window,而不是指向该元素

<input type="button" value="修改" οnClick="changeContent(this)" />

这里的 this 指这个 input 标签,要设置对应的形参才是当前对象那个如果没有设置形参 只是$(this)就是 windows 对象

关于 setInterval 和 setTimeout 中的 this 指向问题

var num = 0;
function Obj() {
    this.num = 1;
    this.getNum = function () {
        console.log(this.num);
    };
    this.getNumLater = function () {
        setTimeout(function () {
        console.log(this.num);
        }, 1000);
    };
}
var obj = new Obj();
obj.getNum(); //1  打印的是obj.num,值为1
obj.getNumLater(); //0  打印的是window.num,值为0

从上述例子中可以看到 setTimeout 中函数内的 this 是指向了 window 对象,这是由于 setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致这些代码中包含的 this 关键字会指向 window (或全局)对象。详细可参考MDN setTimeout

  1. 将当前对象的 this 存为一个变量,定时器内的函数利用闭包来访问这个变量,如下:
var num = 0;
function Obj() {
    var that = this; // 将this存为一个变量,此时的this指向obj
    this.num = 1;
    this.getNum = function () {
    console.log(this.num);
};
this.getNumLater = function () {
    setTimeout(function () {
        console.log(that.num); // 利用闭包访问that,that是一个指向obj的指针
        }, 1000);
    };
}
var obj = new Obj();
obj.getNum(); //1  打印的是obj.num,值为1
obj.getNumLater(); //1  打印的是obj.num,值为1
  1. 利用 bind()(举例) cell() apply() 方法
var num = 0;
function Obj() {
    this.num = 1;
    this.getNum = function () {
        console.log(this.num);
    };
    this.getNumLater = function () {
        setTimeout(function () {
        console.log(this.num);   
        }.bind(this),1000); //利用bind()将this绑定到这个函数上
    };
}
var obj = new Obj();
obj.getNum(); //1  打印的为obj.num,值为1
obj.getNumLater(); //1  打印的为obj.num,值为1

  1. 箭头函数
var num = 0;
function Obj() {
    this.num = 1;
    this.getNum = function () {
        console.log(this.num);
    };
    this.getNumLater = function () {
        setTimeout(() => {
            console.log(this.num);
        }, 1000); //箭头函数中的this总是指向外层调用者,也就是Obj
    };
}
var obj = new Obj();
obj.getNum(); //1  打印的是obj.num,值为1
obj.getNumLater(); //1  打印的是obj.num,值为1

ES6 中的箭头函数完全修复了 this 的指向,this 总是指向词法作用域,也就是外层调用者 obj,因此利用箭头函数就可以轻松解决这个问题。