this绑定的规则

从执行上下文中我们可以知道,函数在运行时会生成函数执行上下文,在这个里面存在着this,而this绑定谁,则由规则去定义。

一共有四条规则,我们按从小到大一一讲解。

默认绑定规则

当其它三条规则不满足时,则使用默认绑定规则,在非严格模式下,this会被指向全局对象,也就是window,但是再严格模式下,会被指向undefined。

function a() {
    console.log(this); //window
}

a();
function a() {
    "use strict";
    console.log(this); //undefined
}

a();

注意这个严格模式声明的位置,它声明在a函数内部,所以在a函数的执行上下文中,this会是undefined,如果你将严格模式定义在全局,那么所有的函数默认绑定就会都是undefined。

"use strict";

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

function b() {
    console.log(this);
}


a();

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,简单点来说,它是否有被调用,比如通过某个对象的属性调用函数。

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

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

但是隐式绑定很容易造成无意间的绑定丢失,也就是说它导致采用默认绑定了。

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

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo; // 函数别名
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

比如我们可能有时候会拿了对象中某个属性方法单独保存使用,但是这样的话,就会改变它的执行上下文对象,从而导致绑定丢失。

还有就是回调函数,某个对象的属性函数可能会作为回调去使用,这就导致函数被单独执行,也会丢失this绑定。

显式绑定

由于隐式容易丢失this的绑定,所以提供了显式指定this的方法,一共有三个:

  1. apply
  2. call
  3. bind

显式指定后就不会再受隐式绑定的影响。

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

var obj = {
    a: 2
};

foo.call(obj); // 2

new绑定

new一个构造函数的时候,他会将函数内的this绑定到新创建的实例对象上,且它是优先级最高的。

需要注意的是new绑定和显式绑定是不能同时存在的,所以不存在冲突情况。

javascript中的new操作符,虽然看起来和其他面向类的语言一样,但是实际在实现上是完全不同的。
在js中的构造函数并不是类,只是一种被new操作符调用的函数而已,所以包括内置的对象函数在内的所有函数,都可以通过new来调用,这是我们常常用开头大写来区分而已,最终只是对于函数的“”构造调用。

是用new操作符会产生以下操作:

  1. 创建一个全新的对象
  2. 这个对象原型链接到构造函数的原型
  3. 这个对象会被函数内的this绑定
  4. 如果函数没有return出其他对象,这个新对象会作为默认值抛出
function foo(a) {
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2

绑定规则优先级

首先new绑定和显式绑定不能同时使用,所以这两个属于最高优先,而隐式绑定第二,最后则是默认绑定。

被忽略的this

在使用显式绑定的方法时,js并没有要求一定要传一个有效的对象作为this绑定的对象,而是允许传入null或者undefined的。

当使用这两个作为绑定对象是,实际上走的是默认规则。

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

var a = 2;
foo.call(null); // 2

但是使用null或者undefined显然会带来一些未知的问题,这种问题甚至难以分析和追踪,为了解决这个问题,在ES5的时候,提供了Object.create创建对象的方法。

这个方法可以创建原子(MDZ)级的对象(周爱民大佬的解释),也就是无prototype的空对象。

通过绑定一个什么也没有的对象,可以避免this被绑定到全局,从而造成的莫名其妙的问题。

function foo(a, b) {
    console.log("a:" + a + ", b:" + b);
} 

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

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

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

bar(3); // a:2, b:3

间接引用

当函数被赋值时,其实也是有返回值的,返回目标函数的引用,就是被赋值的函数本身。

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

此时后三条绑定规则不满足,只能走默认规则。

软绑定

硬绑定这种方式可以把 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;
    };
}
function foo() {
    console.log("name: " + this.name);
}
var obj = {
        name: "obj"
    },
    obj2 = {
        name: "obj2"
    },
    obj3 = {
        name: "obj3"
    };

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 <---- 应用了软绑定

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

箭头函数的this

上述的四条规则只能应用于所有正常的函数,ES6新增了一个箭头函数,他的this绑定规则是不一样的。

箭头函数的this是根据外层(函数或者全局)作用域来决定this!

也就是说他会获取包裹它的作用域中的this。

function foo() {
    // 返回一个箭头函数
    return (a) => {
        //this 继承自 foo()
        console.log(this.a);
    };
}

var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
}

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

当foo函数被显式绑定this为obj1时,才会返回箭头函数,此时箭头函数的this从外层获取,所以也是obj1,而且箭头函数无法被显式指定this绑定,哪怕你使用new操作符,虽然它并不会报错

箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

function foo() {
    var self = this; // lexical capture of this
    setTimeout(function() {
        console.log(self.a);
    }, 100);
}

var obj = {
    a: 2
};

foo.call(obj); // 2

虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替代的是 this 机制。

所以我们可以肯定的说,this的绑定就是在运行时决定的,虽然箭头函数看上去像是书写时决定的,但是它其实获取的是外层的this,外层的this是在运行时在执行上下文环境才有的,所以箭头函数和普通函数他们的this都是运行时决定的。

小结

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. 由 new 调用?绑定到新创建的对象
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。

一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 ? = Object.create(null),以保护全局对象。ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

分类: 你不知道的JavaScript 标签: this严格模式this绑定规则箭头函数

评论

暂无评论数据

暂无评论数据

目录