JS的this绑定

js的this关键字一般有四种绑定,当然也有一些例外绑定。

一、默认绑定

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

foo()是直接使用不带任何修饰的函数引用进行调用的,会使用默认绑定,无法应用其他规则。

默认绑定在非严格模式下,foo()最后会将this设置为全局对象。

在严格模式下,这是未定义的行为。

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

虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定:

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

二、隐式绑定

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

function foo() {
    console.log(this.a);
}

var obj2 = {
    a: 22,
    foo2: foo
};
// 必须限定以obj2
var obj1 = {
    a: 2,
    obj: obj2
};

obj1.obj.foo2();

一个最常见的this绑定问题隐式丢失。是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"

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

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    // fn其实引用的是foo
    fn(); // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo(obj.foo); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。再比如setTimeout之类的函数,也是类似的情况。

三、显示绑定

采用call,apply,bind的方式调用。其中call和apply几乎一样,后者参数是数组列表。可以称之为硬绑定。

四、new绑定

将this设置为一个全新的空对象。

首先我们重新定义一下JavaScript中的“构造函数”。在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。这点和其他语言的new有很大不同。这里有一个重要但是非常细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

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

  • 1)创建(或者说构造)一个全新的对象。
  • 2)这个新对象会被执行[[Prototype]]连接。
  • 3)这个新对象会绑定到函数调用的this。
  • 4)如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

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

优先级如下:

  • 1)函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
  • 2)函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
  • 3)函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
  • 4)如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。不过……凡事总有例外。

五、例外绑定

1. 被忽略的this

如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

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

然而,总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致不可预计的后果(比如修改全局对象)。一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized zone,非军事区)对象——它就是一个空的非委托的对象。如果我们在忽略this绑定时总是传入一个DMZ对象,那就什么都不用担心了,因为任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。

function foo(a, b) {
    console.log("a:" + a + ", b:" + b);
}
// 我们的DMZ空对象
var ø = Object.create(null);
// 把数组展开成参数
foo.apply(ø, [2, 3]); // a:2, b:3
// 使用bind(..)进行柯里化
var bar = foo.bind(ø, 2);
bar(3); // a:2, b:3

使用变量名ø不仅让函数变得更加“安全”,而且可以提高代码的可读性,因为ø表示“我希望this是空”,这比null的含义更清楚。不过再说一遍,你可以用任何喜欢的名字来命名DMZ对象。

2. 间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

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

赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。根据我们之前说过的,这里会应用默认绑定。注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。

3. 软绑定

硬绑定这种方式可以把this强制绑定到指定的对象(除了使用new时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改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;
    };
}

除了软绑定之外,softBind(..)的其他原理和ES5内置的bind(..)类似。它会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。此外,这段代码还支持可选的柯里化。

4. this词法

我们之前介绍的四条规则已经可以包含所有正常的函数。但是ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。我们来看看箭头函数的词法作用域:

function foo4() {
    // 返回一个箭头函数
    // return function() { //这种返回则会返回3
    return () => {
        //this继承自foo()
        console.log("this.a:", this.a);
    };
}

var obj1 = {
    a: 2,
};

var obj2 = {
    a: 3,
};

var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是3!

foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1, bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)

再看一个例子:

//!!! 箭头函数的this从外层代码库继承,所以箭头函数的this是在定义的时候就绑定好了的,而普通函数是在调用的时候确定this指向
console.log("EX15 =============>");
const obj = {
    fun1: function () {
        console.log(this);
        return () => {
            console.log(this);
        };
    },
    fun2: function () {
        return function () {
            console.log(this);
            return () => {
                console.log(this);
            };
        };
    },
    // 箭头函数声明的时候,已经确定了this的绑定
    fun3: () => {
        console.log(this);
    },
};

// 这里明显进行的是隐式绑定,fun1的this指向obj
let f1 = obj.fun1(); // obj
// 这里执行了上一行返回出来的箭头函数,我们分析上一层代码库的this指向obj,所以直接继承,箭头函数this指向
f1(); // obj

// fun2第一层执行的时候没有打印代码,而是返回了一个函数出来,赋值给f2,并且这里发生了绑定丢失,
// this指向由原来的obj指向了window(发生了赋值)
let f2 = obj.fun2();

// f2()执行了,打印出了改绑后的this——window,然后将箭头函数返回出来,赋值给f2_2
let f2_2 = f2(); // window

// 执行打印出window,刚才的外层代码的this不是指向了window吗,所以这里就继承了window作为this
f2_2(); // window

// 在字面量中直接定义的箭头函数无法继承该对象的this,而是往外再找一层,就找到了window,
// 因为字面量对象无法形成自己的一层作用域,但是构造函数可以哦。
obj.fun3(); // window, vscode环境为local

// 那我们怎么操纵箭头函数的this指向呢? 答案是修改外层代码库的this指向,在箭头函数定义之前就给this修改方向即可。
console.log("修改this绑定。。。。");

// fun2: function () {
//   return function () { // 我们修改的是这里的this
//     console.log(this);
//     return () => { // 然后这里定义的时候就继承啦
//       console.log(this);
//     }
//   }
// },
let fun4 = f2.bind(obj)(); // obj
fun4(); // obj

最后更新于 2023-11-17 16:30:12 by twotwolucky

发表评论