前言
-
JavaScript并不具有动态作用域,只有词法作用域。但是this机制某种程度上很像动态作用域。
-
this关键字时JavaScript中最复杂的机制之一。它被自动定义在所有函数的作用域中。
一、为什么使用this
先来看一段代码,常见使用this的写法
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call(this);
console.log(greeting);
}
var me = {
name: "Kyle";
};
var you = {
name: "Reader";
}
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, 我是KYLE
speak.call(you); // Hello, 我是READER
- 这段代码可以在不同的上下文对象(me和you)中重复使用函数
identify()
和speak()
,不用针对每个对象编写不同版本的函数。 - 如果不使用this,那就需要给
identify()
和speak()
显式传入一个上下文对象。
不使用this的写法:
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting)
}
identify(you); // READER
speak(me); // hello,I’m KYLE
- 通过对比可以看出,this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更简洁并且易于复用。
- 这里就体现this的好处:随着你使用模式越来越复杂,显示传递上下文对象会让代码变得越来越混乱,使用this则不会。
二、调用栈和调用位置
- this不是指向函数本身,或者函数的词法作用域。
- this是在运行时绑定的,它的上下文取决于函数调用时的各种条件。
- 当一个函数被调用时,会创建一个活动记录(也称上下文)。
- 这个记录会包含函数在哪里被调用(调用栈),函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行过程中用到。
下面代码说明什么是调用栈和调用位置:
function baz() {
// 当前调用栈是:baz
// 因此,当前的调用位置是全局作用域
console.log("baz");
bar();
}
function bar() {
// 当前调用栈是baz -> bar
// 因此,当前调用位置在baz中
console.log("bar");
foo(); // foo的调用位置
}
function foo() {
// 当前的调用栈是baz -> bar -> foo
// 因此,当前的调用位置在bar中
console.log("foo");
}
baz(); // baz的调用位置
可以把调用栈想象成一个函数调用链。使用浏览器调试工具可以查看到函数调用列表。
三、绑定规则
先找到调用位置,然后判断需要应用下面四条规则的哪一条。
1. 默认绑定
函数直接调用
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
this.a被解析成全局变量a。严格模式下则不是。
2. 隐式绑定
function foo() {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
};
obj.foo() // 2
- 调用位置使用obj来引用函数,或者说作为对象的方法进行调用。
- 函数调用中的this被绑定到obj上,因此this.a和obj.a是一样的。
隐式丢失
function foo() {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;
var a = "opps, global";
bar(); // opps, global
这里函数的调用跟obj没有关系了,虽然obj对函数存在引用,但是函数不是作为对象的方法调用。
回调函数丢失this绑定
function foo() {
console.log(this.a)
}
var obj = {
a: 2;
foo: foo
};
var a = "opps, global"
setTimeout(obj.foo, 100); // "opps, global"
这是因为参数传递其实就是一种隐式赋值。
上面代码中的定时器:
function setTimeout(fn, delay) { // 这里相当于var fn = obj.foo
// 等待delay毫秒
fn(); //调用位置
}
- this的改变是意想不到的。实际上,你无法控制回调函数的执行方式。
- 因此就没有办法控制调用位置以得到期望的绑定。之后会通过固定this来修复这个问题。
3. 显式绑定
使用call()、apply()和bind()
function foo() {
console.log(this.a);
}
var obj = {
a: 2;
}
foo.call(obj); // 2
通过call可以在调用foo()时,强制把它的this绑定到obj上。
4. 绑定例外
- 如果把
null
和undefined
作为this的绑定对象传入call,apply或者bind。 - 这些值在调用时会被忽略,实际应用的时默认绑定规则。
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null) // 2
- 但是这样会有副作用,更安全的时传入空对象
Object.create(null)
,可以避免不必要的this绑定。
5. 箭头函数
箭头函数根据外层(函数或全局)作用域来决定this,且无法修改。
function foo() {
setTimeout(() => {
// 这里的this在词法上继承自foo()
console.log(this.a)
}, 100);
}
var obj = {
a: 2;
};
foo.call(obj); // 2
- 实际上,在ES6之前,经常使用一种与箭头函数几乎一样的模式。
- 就是使用一个变量保存this,并向下传递。
function foo() {
var self = this;
setTimeout(() => {
console.log(self.a);
}, 100);
}
var obj = {
a: 2;
};
foo.call(obj); // 2
四、总结
1. this机制
this的作用:this提供了一种更优雅的方式来隐式“传递”一个对象引用。(避免传递上下文)
其实this执行主要三种情况,
- 直接调用函数,this指向window,如果是严格模式指向的是undefined。
- 作为对象的方法调用,this指向该对象。
- 箭头函数没有自己this,this指向与上一级作用域中的this一致,且不能修改。
call、apply、bind、new这几个修改this执行都是类似的,使用的是第二种情况,将函数作为传入的对象(new操作时是新建的对象)的方法调用,从而改变this指向。
注意:函数对参数存在隐式赋值,如果将函数作为参数传入,无法确定this指向。
this绑定的优先级:
- 箭头函数 > new > 显式 > 隐式 > 默认绑定
可以查看这几个方法的实现:
2. call
Function.prototype.myCall = function (context,...args) {
// 判断
if(typeof this !== 'function'){
throw new Error(`${this} is not function`)
}
if(!context instanceof Object){
throw new Error(`${context} is not Object`)
}
// 关键两步:
// 将函数绑定到对象上作为对象上的方法,这里的this就是调用call方法的函数
context.fn = this
// 作为对象对象中的方法指向
const result = context.fn(...args)
//删除被绑定的方法
delete context['fn']
return result
}
3. apply
Function.prototype.myApply = function (context,args) {
// 判断
if(typeof this !== 'function'){
throw new Error(`${this}is not function` )
}
if(!context instanceof Object){
throw new Error(`${context} is not Object`)
}
if(args && !Array.isArray(args)){
throw new Error(`${args} is not Array`)
}
// 主要两步:
//将函数绑定到对象上作为对象上的方法
context.fn = this
//执行对象中的方法
const result = context.fn(...args)
return result
}
可以看到apply()
和bind()
的实现很类似,主要区别在于apply()第二个参数接受数组。
4. bind
Function.prototype.myBind = function (context) {
// 保存this
const self = this;
// 判断
if (typeof self !== "function") {
throw new Error(`${self}is not function`);
}
if (!context instanceof Object) {
throw new Error(`${context} is not Object`);
}
//bind方法返回一个函数,这里相当于返回一个bind方法
return function (...args) {
//将函数绑定到对象上作为对象上的方法
context.fn = self
//执行对象中的方法
const result = context.fn(...args);
return result;
};
};
bind和前面两种方法的区别:bind()返回一个函数,这个函数的主要功能和bind()
一样。
5. new
- new操作过程中先新建一个对象,然后将构造函数作为对象的方法调用。
- 修改this指向的方式和前面几种方法类似,只是多了其他内容。
后续在原型对象中进行分析。