有关 this 的认知
this 是什么
this 是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。且在不同场合使用的函数,其 this 的值也不同。总的来说,this 就是函数运行时所在的环境对象。
this 指向
this 既不指向函数自身也不指向函数的词法作用域。
this 的绑定
this 是在运行时进行绑定的,而不是在编写时,且它的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。同时,this 的上下文取决于函数调用时的各种条件。
调用位置
函数的调用位置决定 this 的绑定对象。因此,在理解 this 的绑定规则之前,我们需要先理解函数的调用位置(不是声明的位置): 在当前正在执行的函数的前一个调用中。
而要找出调用位置,则需要分析调用栈。下面,我们一起来看一个简单的例子:
// 当前例子出自《你不知道的JavaScript》上卷——调用位置章节
function baz() {
// 当前调用栈: baz
// 当前调用位置: 全局作用域
console.log("调用栈: baz");
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈: baz -> bar
// 当前调用位置: baz
console.log("调用栈: bar");
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈: baz -> bar -> foo
// 当前调用位置: bar
console.log( "调用栈: foo" );
}
baz(); // <-- baz 的调用位置
下图展示的是上述代码在执行过程中的调用栈( Call Stack )
通过阅读上面的图文,想必大家对调用栈和调用位置已有所理解。下面,我们就一起来看看 this 的绑定规则。
this 绑定规则
this 的绑定规则有四条:
- 默认绑定
- 隐式绑定
- 显式绑定
- new绑定
this 的绑定,发生在函数被调用时,它指向什么完全取决于函数在哪里被调用。在分析使用那种绑定规则时,一定要明确函数调用位置。下面就让我们一起来看看这四条规则吧。
默认绑定
案例代码
// 在全局作用域中定义的全局变量 a,会成为全局对象的一个同名属性
var a = 2;
function foo() {
console.log('this.a--->', this.a);
console.log('this-->', this);
}
foo();
结果展示
上面这种函数调用,通常称为独立函数调用(也叫纯粹的函数调用),是函数的最通常用法,属于全局性调用。它使用了 this 的默认绑定,因此,函数中的 this 指向全局对象(注意,严格模式下无法使用默认绑定,this 指向 undefined)。
如何就认定 foo() 函数是应用了默认绑定的呢?注意观察它的调用位置。在上例代码中,foo() 直接使用不带任何修饰的函数引用进行调用的,它无法应用其他规则,所以它使用了默认绑定。
使用默认绑定规则,往往意味着无法应用其他规则。所以,我们可以把这条规则看作是无法应用其他规则时的默认规则。
隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。所以,在判断一个被调用的函数是否应用隐式绑定规则时,就看其调用位置是否有上下文对象。
案例代码
let obj = {
num: 2,
getNum: function () {
console.log('this.num--->', this.num);
console.log('this--->', this);
}
};
obj.getNum();
结果展示
隐式绑定注意事项:
- 对象属性引用链中只有最顶层或者说最后一层会影响调用位置。看下面这个例子,函数中的 this 会指向 obj2,而不是 obj1。
案例代码
let obj2 = {
num: 10,
getNum: function () {
console.log('this.num--->', this.num);
console.log('this--->', this);
}
};
let obj1 = {
num: 1,
obj2: obj2
};
obj1.obj2.getNum();
*结果展示
- 隐式丢失
这个问题指的是:被隐式绑定的函数会丢失绑定对象。换句话说,就是它会应用默认绑定。注意,默认绑定会将 this 绑定到全局对象或 undefined 上(这取决于代码是否启用了严格模式)。
案例代码 一
function getNum() {
console.log("this.a--->", this.a);
console.log("this--->", this);
}
var a = "hello world"; // a 是全局对象的属性
var obj = {
a: 2,
getNum: getNum
};
var alias = obj.getNum;
alias();
此代码中的 alias 是 obj.getNum 的一个引用,但它引用的是 getNum 函数本身。因此,当调用 alias() 时,其实是在调用一个不带任何修饰的函数,所以就应用了默认绑定规则。
*结果展示
案例代码 二
function getNum() {
console.log("this.a--->", this.a);
console.log("this--->", this);
}
function done(fn) {
// fn 实际上引用的是 getNum
fn(); // <-- 调用位置
}
var a = "另一种隐式丢失"; // a 是全局对象的属性
var obj = {
a: 2,
getNum: getNum
};
done(obj.getNum); // "另一种隐式丢失"
参数传递其实就是一种隐式赋值,不会因为传入的参数是函数而变的特殊。所以此案例和上一个例子没多大差别。将函数当作参数传递的情况(即回调函数)非常常见,但我们无法控制回调函数的执行方式,因此也就没法控制会影响绑定的调用位置,稍不注意就会造成隐式丢失问题。
*结果展示
另外,将我们声明的函数传入语言内置的函数(例如,setTimeout),结果是一样的,没有区别。这个情况,就留给大家自己调试吧。
显式绑定
call 和 apply
同学们对 call() 和 apply() 应该不会陌生,它们是函数上的方法,JavaScript 提供的大部分函数和我们自己创建的函数都能够使用这两个方法。这两个方法的第一个参数都是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为能直接指定 this 的绑定对象,所以我们称这种绑定为显式绑定。
案例代码
function getNum() {
console.log("this.a--->", this.a);
console.log("this--->", this);
}
var a = '显示绑定到——全局对象';
var obj = {
a: "显示绑定到——指定对象",
};
// 从 this 绑定的角度来说,call() 和 apply() 是一样的,所以这里仅用 call 做测试
getNum.call(obj);
console.log("--------------------------------");
getNum.call();
注意,call() 或 apply() 的参数为 null(或空)时,默认调用全局对象。另外,若是你给它们传入一个原始值当作 this 的绑定对象,那么这个原始值会被转换成它的对象形式( 例如,new Number() )。这通常被称为“装箱”。
*结果展示
硬绑定
硬绑定是一种显式的强制绑定。它能解决显式绑定无法解决的丢失绑定问题。
案例代码
function getNum() {
console.log("this.a--->", this.a);
console.log("this--->", this);
}
function handle() {
getNum.call(obj);
}
var obj = { a: 2 };
handle();
console.log("---------------------");
handle.call(window); // 硬绑定的 handle 不会再修改它的 this
console.log("---------------------");
setTimeout(handle, 100);
由于在 handle() 函数的内部手动调用了 getNum.call(obj),因此强制把 getNum的 this 绑定到了 obj 对象上。之后无论如何调用函数 handle,它都会在 obj 上调用 getNum。
*结果展示
通过上面所举的一个简单的硬绑定示例,也许你已注意到,硬绑定是包裹在一个函数内的。事实上,硬绑定的典型应用场景就是创建一个包裹函数。
function getNum() {
console.log(this.a);
}
// 一个辅助绑定函数
function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
};
}
var obj = { a: 2 };
var handle = bind(getNum, obj);
handle(); // 2
实际上,在 ES5 中就提供了一个硬绑定的内置方法——Function.prototype.bind
。bind 方法会返回一个硬编码的新函数,且它会将参数设置为 this 的上下文并调用原始函数。
案例代码
function getNum() {
console.log("this.a--->", this.a);
console.log("this--->", this);
}
var obj = { a: 2 };
var fn = getNum.bind(obj);
fn(); // 2
console.log("返回一个硬编码函数--->", getNum.bind);
console.log("fn--->", fn);
*结果展示
new绑定
在 JavaScript 中,当我们使用 new 来调用函数时,就会应用 new 绑定规则。而通过 new 操作符调用的函数,则被称为构造函数(实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”)。
当使用 new 来调用函数时,会自动执行以下操作(参考于《你不知道的JavaScript》上卷):
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
案例代码
function Foo(a) {
this.a = a;
}
var f = new Foo(2);
console.log( f.a ); // 2
*结果展示
在此例代码中,当我们使用 new 操作符调用 Foo() 时,就会构造一个新对象并把它绑定到 Foo() 调用中的 this 上。
主要参考来源
-
《你不知道的JavaScript》上卷