JS this

this关键字是JS中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。

为什么要使用this

this提供了一种更优雅的方式来隐式 ”传递“ 一个对象引用,因此可以把API设计得更加简洁且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用this则不会这样。

this是在运行时进行绑定的,他的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

this既不指向函数自身也不指向函数的词法作用域,this实际上是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

调用位置

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log("baz");
    bar();
}
function bar() {
    // 当前调用栈是:baz - bar
    // 因此,当前调用位置是baz
    console.log("bar");
    foo();
}
function foo() {
    // 当前调用栈是:baz - bar - foo
    // 因此,当前调用位置是bar
    console.log("foo");
}
baz();

绑定规则

函数的执行过程中调用位置如何决定this的绑定对象:你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。

默认绑定

最常见的函数调用类型:独立函数调用。

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

当调用foo时,this.a被解析成全局变量a。为什么?因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象。

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

如果使用严格模式,则不能将全局对象用于默认绑定,this会绑定到undefined

function foo() {
    "use strict"
    console.log(this.a);
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

这里有一个微妙但非常重要的细节,虽然this的绑定规则完全取决于调用位置,但只有foo运行在非严格模式下时默认绑定才能绑定到全局对象,在严格模式下调用foo不影响绑定。

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

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说被某个对象拥有或者包含。

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

foo 被调用时,他的前面加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因此,this.aobj.a 是一样的。

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

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

隐式丢失

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

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

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

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    fn();
}
var obj = {
    a: 2,
    foo: foo
}
var a = "global";
doFoo(obj.foo); // "global" , 无论全局变量a是在obj之前还是之后声明并赋值

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。如果把函数传入语言内置函数而不是传入自己声明的函数,结果也是一样的。

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

如上,回调函数丢失this绑定是非常常见的,此外,调用回调函数还可能会修改this

显式绑定

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

具体来说,可以使用callapply方法。这两个方法如何工作呢,他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。

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

可惜,显示绑定仍然无法解决丢失绑定的问题。

硬绑定

硬绑定可以解决丢失绑定的问题。

我们创建函数bar并在内部手动调用foo.call(obj),因此强制把foothis绑定到了obj,之后无论如何调用函数bar,他总会在obj上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

function foo() {
    console.log(this.a);
}
var obj = { a: 2 };
var bar = function() {
    foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的bar不可能再修改this
bar.call(window) // 2

硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = { a: 2 };
var bar = function() {
    return foo.apply(obj, arguments);
}
var b = bar(3); // 2 3 
console.log(b); // 5

另一种是创建一个可以重复使用的辅助函数

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    };
}
var obj = { a: 2 };
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); //5

由于硬绑定是一种非常常用的模式,所有ES5提供了内置的方法Function.protoprototype.bind

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = { a: 2};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

new 绑定

JS中,构造函数只是一些使用new操作符时被调用的函数,他们并不会属于某个类,也不会实例化一个类。实际上,他们甚至不能说是一种特殊的函数类型,他们只是被new操作符调用的普通函数而已。

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

使用new来调用foo时,我们会构造一个新对象并把它绑定到foo调用中的this上。

优先级

如果某个调用位置应用了多条规则怎么办?为了解决这个问题就必须给这些规则设定优先级。

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注