【每日笔记 · 3】this全面解析

289 阅读4分钟

1.调用位置

this完全取决于函数的调用位置。

调用位置是函数在代码中被调用的位置。

调用栈是为了到达当前执行位置所调用的所有函数,调用位置就在当前正在执行的函数的前一个调用中。

function baz() {
    // 当前调用栈: baz
    // 当前调用位置: 全局作用域
    bar();
}

function bar() {
    // 当前调用栈: baz => bar
    // 当前调用位置: baz
    foo();
}

function foo() {
    // 当前调用栈: baz => bar => foo
    // 当前调用位置: bar
    console.log('foo');
}

2.绑定规则

this的绑定对象需要找到调用位置,然后判断需要应用下面四条规则中的哪一条。

  • 默认绑定
独立函数调用。在非严格模式下,默认绑定将this绑定到全局对象;严格模式下,默认绑定将this绑定到undefined。

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

var a = 2;
foo(); // 非严格模式: 2;严格模式: TypeError: this is undefined;
  • 隐式绑定
调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

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

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

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

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

var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42

隐式丢失:隐式绑定的函数会丢失绑定对象,会应用默认绑定。

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

var obj = {
    a: 2,
    foo: foo,
};
var bar = obj.foo; // 函数别名
var a = "我是全局变量a";
bar(); // 我是全局变量a
// bar是obj.foo的一个引用,实际上,它引用的是foo函数本身。
// 此时的bar()其实是一个不带任何修饰的函数调用,因此应用 默认绑定

参数传递也是一种隐式赋值。eg: 回调函数。

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

function doFoo(fn) {
    fn();
}

var obj = {
    a: 2,
    foo: foo
};
var a = "我是全局变量a";
doFoo(obj.foo); // 我是全局变量a

  • 显式绑定
call、apply:他们的第一个参数是一个对象,是给this准备的,在调用函数时将其绑定到this。

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

var obj = {
    a: 2
};
foo.call(a); // 2
  • 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绑定>显式绑定>隐式绑定>默认绑定

3.绑定例外

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

使用场景:

function foo(a,b) {
    console.log(a + "+" + b);
}
// 把数组“展开”成参数
foo.apply(null,[2,3]); // 2 + 3
// 使用bind(...)进行柯里化
var bar = foo.bind(null,2);
bar(3); // 2 + 3

总是使用null来忽略this绑定可能产生一些副作用,会将this绑定到全局对象。一种更安全的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用——创建一个DMZ对象(一个空的非委托对象)。

Object.create(null); // {}相似,但不会创建Object.prototype这个委托,比{}更‘空’

  • 间接引用

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

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

if(!Function.prototype.softBind) {
    Function.prototype.softBind = function (obj) {
        var fn = this;
        var curried = [].slice.call(arguments,1); // 捕获所有curried
        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;
    }
}

4.箭头函数

箭头函数根据外层作用域来决定this。和之前的self = this机制一样。

function foo() {
    return a => {
        console.log(this.a); // this继承自foo
    };
}

var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
};
var bar = foo.call(obj1);
bar.call(obj2); // 2;

5.经典题解

  • this指向

var num = 1;
var myObject = {
    num: 2,
    add: function() {
        this.num = 3; // 隐式绑定 修改 myObject.num = 3
        (function() {
            console.log(this.num); // 默认绑定 输出 1
            this.num = 4; // 默认绑定 修改 window.num = 4
        })(); // 立即执行函数
        console.log(this.num); // 隐式绑定 输出 3
    },
    sub: function() {
        console.log(this.num) // 因为丢失了隐式绑定的myObject,所以使用默认绑定 输出 4
    }
}
myObject.add(); // 1 3
console.log(myObject.num); // 3
console.log(num); // 4
var sub = myObject.sub;//  丢失了隐式绑定的myObject
sub(); // 4

  • 深入this指向

// 1、赋值语句是右执行的,此时会先执行右侧的对象
var obj = {
    // 2、say 是立即执行函数
    say: function() {
        function _say() {
            // 5、输出 window
            console.log(this);
        }
        // 3、编译阶段 obj 赋值为 undefined
        console.log(obj);
        // 4、obj是 undefined,bind 本身是 call实现,
        return _say.bind(obj);
    }(),
};
obj.say();