this 的绑定规则 - 下 - 你不知道的 JS

  1. 隐式和 new 绑定的优先级
  2. 硬绑定和 new 绑定的优先级
  3. 绑定顺序
  4. 例外
    1. this 被故意忽略
    2. 间接引用
    3. 软绑定

毫无疑问,默认绑定的优先级是四条规则中最低的。显式绑定优先于隐式绑定。

隐式和 new 绑定的优先级

function foo(n) {
    this.age = n;
}

var age = 999;

let obj = {
    age: 3,
    foo
}

let a = new obj.foo(6666);
console.log(obj.age); // 3
console.log(a.age);  // 6666

可以看到 new 绑定比隐式绑定优先级高。

硬绑定和 new 绑定的优先级

Function.prototype.bind(..) 会创建一个新的包裹函数,这个函数会忽略它当前的 this 绑定(无论绑定的对象是什么),并把我们提供的对象绑定到 this 上。
看起来硬绑定优先级高于 new 绑定。

function foo(something) {
    this.a = something;
}

var obj1 = {};
var bar = foo.bind( obj1 );

barr( 2 );  // 修改的是 obj 的 a
console.log( obj1.a ); // 2

var bazz = new bar(3);  
console.log( obj1.a ); // obj 的 a 还是 2!!
console.log( bazz.a ); // 3

虽然 bar 被硬绑定到了 obj1 对象上,但是对 bar 进行 new bar(3) 并没有像我们预计的那样把 obj 的 a 修改为 3。就好像绑定的 this 和 new 创建的 this 不是一个 this。
为什么?
其实,是由于 bind 的 JavaScript 的内部实现,会判断 bind 后的函数是否被 new 调用,如果是的话就会创建新的 this 覆盖硬绑定的 this。

绑定顺序

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

不过…… 凡事总有例外。

例外

this 被故意忽略

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

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

在 ES5 中,一种非常常见的做法是使用 apply(..) 来 “展开” 一个数组,并当作参数传入一个函数。类似地,bind(..) 可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用。

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

如果函数并不关心 this 的话,你仍然需要传入一个占位值,这时 null 可能是一个不错的选择,就像代码所示的那样。

只有当你确定某个函数的 this 可以执行默认绑定时,再用 bull 绑定忽略 this。

或者,你可以使用一个真正的空对象来代替 null,作为函数的 this,避免在执行默认绑定时,影响全局的 this。
这个真正的空对象就是通过 Object.create(null) 创建的,虽然和 {} 很像,但是这个对象没有 prototype。

let kull = Object.create(null);
kull.prototype  // undefiend
let obj = {
    a :2
}
kull.__proto__ = obj;
kull.a  // undfined
kill.a = 3
kull.a // 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

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是
p.foo() 或者 o.foo()。相当于直接调用了 foo 函数,因此执行默认绑定

软绑定

硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。

如果可以给默认绑定指定一个 ** 非全局对象和 undefined 值 **,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。

if (!Function.prototype.softBind) {
    Function.prototype.softBind = functtion(obj) {
        let fn = this;
        // 得到第二个 - 最后一个参数
        // 注意区分这里的 arguments 和下面的 arguments 不是一个!
        // 这里的 arguments 是调用 softBind 传入的参数
        let curried = Array.prototype.slice.call(arguments, 1);
        let bound = function () {
            return fn.apply(
                // this 不存在或者 this 是全局对象,则修改 this 为你指定的 obj,否则不修改!
                (!this || this === (window || global)) ?
                    obj :
                    this,
                // 这里的 arguments 是调用 bound 函数传入的参数
                curried.concat.apply(curried, arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    }
}

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

var a = "全局的 a 变量"

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

let obj1 = {
    a: '对象 1 的 a 变量'
}
let obj2 = {
    a: '对象 2 的 a 变量'
}

let f = foo.softBind(obj1);
f() // 对象 1 的 a 变量
obj2.foo2 = foo.softBind(obj1);
obj2.foo2()  // 对象 2 的 a 变量

可以看到,软绑定时 this 可以手动改绑到 obj2 上,如果代码想应用默认绑定,则会绑定到 obj1 上,避免绑定到全局对象上。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论。
我的空间