JS 中的 this,总是神神叨叨的,不小心就错了。
希望我自己写完本文之后,以后也按着现在捋顺的逻辑来分析 this。
TL;DR
- 箭头函数的
this
,和它书写的位置密切相关,在书写阶段(即声明位置)就绑定到它父作用域的this
- 构造函数的
this
,会绑定到我们new
出来的这个对象上 - 在不使用
call/apply/bind
改变this
指向的时候,普通函数的this
,只在调用的时候,绑定到调用方,和它的位置没有关系- 立即执行函数、setTimeout、setInterval 内部是普通函数的时候,因为其调用方是
window
,所以this
是window
- 立即执行函数、setTimeout、setInterval 内部是普通函数的时候,因为其调用方是
- call/apply/bind 均可改变 this 指向,且均可被手写实现
箭头函数里的this
箭头函数的this
,和它书写的位置密切相关,在书写阶段(即声明位置)就绑定到它父作用域的 this
。
因为其由父作用域决定,所以父作用域至关重要。
- 若箭头函数的父作用域是全局作用域,则 this 始终指向
window
- 若箭头函数的父作用域是函数作用域,则指向函数作用域的
this
var a = 1;
var obj = {
a: 2,
func2: () => {
// 父作用域是全局,this始终都是window
console.log(this.a);
},
func3: function() {
// 等同于func3作用域的this
() => {
console.log(this.a);
};
}
};
// func1
var func1 = () => {
// 父作用域是全局,this任何时候都是window
console.log(this.a);
};
// func2
var func2 = obj.func2;
// func3
var func3 = obj.func3;
func1();
func2();
func3();
obj.func2();
obj.func3();
构造函数里的 this
构造函数里面的 this 会绑定到我们 new 出来的这个对象上:
function Person(name) {
this.name = name;
console.log(this);
}
// this是person
var person = new Person("yan");
function 定义的普通函数
function 定义的普通函数,this 的指向是在调用时决定的,而不是在书写时决定的。这点和闭包恰恰相反。
换言之:不管方法被书写在哪个位置,它的 this 只会跟着它的调用方走,
再大白话点:xx.fn()
,点前面是谁,fn 里的 this 就是谁。没有点就是 window。
注意上面的前提是:不使用
call/apply/bind
改变this
指向的时候。
必须严格区分 “声明位置” 与 “调用位置”!!!
上面的秘诀掌握好了,下面的例子就是 easy 了
// 声明位置
var me = {
name: "yan",
hello: function() {
console.log(`你好,我是${this.name}`);
}
};
var you = {
name: "xiaoming",
hello: function() {
var targetFunc = me.hello;
targetFunc();
}
};
var name = "BigBear";
// 调用位置
you.hello();
还有 2 种特殊点的情景:
没有调用方,所以 this 始终都是window
- 立即执行函数(IIFE),
(function(){})()
- setTimeout/setInterval 中传入的普通函数,
setTimeout(function(){...},1000)
!!!注意,必须是 function 写的普通函数,如果箭头函数,仍遵循箭头函数的规则
先看个立即执行函数的例子,
var name = "BigBear";
var obj = {
name: "yan",
fn: function() {
(function() {
console.log(this.name);
})();
}
};
obj.fn();
仔细看看就是自执行函数执行,this 肯定是 window,自然就是BigBear
。
注意!!!换成箭头函数的话,规则就变了,因为父作用域是 fn,其 this 绑定成了 obj,所以打印的话是yan
var obj = {
fn: function() {
(() => {
console.log(this);
})();
}
};
obj.fn();
再看下 setTimeout
var name = "BigBear";
var me = {
name: "yan",
hello: function() {
setTimeout(function() {
console.log(`你好,我是${this.name}`);
});
}
};
me.hello();
setTimeout 里面的函数,this 指向window
,自然值是BigBear
。
同理,如果将 setTimeout 里面的函数修改成箭头函数,则打印yan
了。
var name = "BigBear";
var me = {
name: "yan",
hello: function() {
setTimeout(() => {
console.log(`你好,我是${this.name}`);
});
}
};
me.hello();
严格模式下的 this
笔者不用严格模式,真要用的时候百度下吧。
改变 this 的指向
this 的指向,要么被书写位置限制,要么被调用位置限制,很是被动。
- 对于箭头函数,因为其
this
只和书写位置有关,所以一般不修改箭头函数的 this 指向。 - 构建函数,this 就是 new 出来的实例,所以一般不修改构建函数的 this 指向
- 于是重点!!!对于 function 定义的普通函数,修改 this,必须要显示的调用
call/apply/bind
。
一般问的修改 this,也是指 function 定义的普通函数。
call/apply/bind
三者用法及区别:
- call/apply 改变函数的 this,且函数立即执行。但 apply 的参数是数组,call 的参数是非数组
- bind 改变函数的 this,但函数未执行
var init = 0;
function add(num1, num2) {
console.log(this.init + num1 + num2);
}
// 普通执行,肯定输出3
add(1, 2);
var obj = { init: 100 };
// 此时因为this变成obj,所以输出103
add.apply(obj, [1, 2]);
// 此时因为this变成obj,所以输出103
add.call(obj, 1, 2);
// 此时因为this变成obj,但需要调用一次函数才行,bind本身返回的是函数
add.bind(obj, 1)(2);
手写实现 call/apply/bind
其实细看下 call,发现 call 有以下特征:
- call 是函数的方法,可以被函数直接调用
- call 第一个参数是 this 绑定的对象,后面的参数是函数的参数
- call 调用之后,函数执行,但 this 绑定到第一个参数上
推理下 call 怎么实现:
- 是函数的方法,每个函数都可调用,可写在 Function.prototype 上
- 参数区别第一个参数,和后面的参数
- call 内,this 绑定到第一个参数上,函数执行
其实想想普通函数 this 的指向只和调用方有关
=> 于是 obj.fn()
=> 但是 fn 并不是 obj 的方法
=> 于是给 obj 增加 fn 这个方法不就行了
=> 但是 call 执行完,必须再将 fn 这个方法删掉就好了
Function.prototype.myCall = function(context, ...args) {
if (context === null) {
return fn(...args);
}
// 注意 这里的this就是调用call的函数
const fn = this;
// 先增加
context.fn = fn;
// 执行
return fn(...args);
// 在删除
delete context.fn;
};
// 测试下,没问题,输出103
add.myCall(obj, 1, 2);
同理:
// 注意因为参数args就是数组,所以不要加...
Function.prototype.myApply = function(context, args) {
if (context === null) {
return fn(...args);
}
// 注意 这里的this就是调用myApply的函数
const fn = this;
context.fn = fn;
return fn(...args);
delete context.fn;
};
// 同理输出103
add.myApply(obj, [1, 2]);
Bind 稍微复杂点:
- bind 是函数的方法,可以被函数直接调用
- bind 返回一个原函数的拷贝,但 this 被指定
- bind 也可以传原函数的部分参数
其实 bind 就是返回一个函数,函数内部执行 call~
Function.prototype.myBind = function(context, ...frontArgs) {
const fn = this;
return function(...behindArgs) {
return fn.call(context, ...frontArgs, ...behindArgs);
};
};
// this被指定,但是需要调用一次才能执行函数,输出103
add.myBind(obj, 1)(2);