相信你一定看过很多关于this指向,手写apply、call、bind实现的文章,我也一样,其中有些全篇通透,有些晦涩难懂,每个人学习的路上都能看到不同的风景,我也愿意将我的所见所闻分享给大家,如有不足之处请在评论区指正。
this的指向
一句话:this永远指向最后调用它的对象,虽然这句话并不完全正确,但是已经能够让你充分理解this的指向,话不多说,直接上例子
例1: 全局上下文调用函数
调用函数a的地方是最外层对象window,所以 this.name 打印的是window上的name属性 windowsName
let name = 'windowsName';
function a(){
let name = 'Li Baitian';
console.log(this.name) // windowsName
console.log("inner:" + this); // inner Window
}
a();
console.log("outer:" + this) // outer Window
再看另外一个例子:调用函数a的地方是全局上下文(window),无论是在严格模式还是非严格模式下,全局上下文中的this都是指向最顶层对象的。但是,对于例1的代码,如果使用严格模式则会报错 Uncaught TypeError: Cannot read property 'name' of undefined 因为在严格模式下必须明确指定函数的调用者,如果不明确指定,则this为undefined。
'use strict'
let name = 'windowsName';
function a(){
let name = 'Li Baitian';
console.log(this.name) // windowsName
console.log("inner:" + this); // inner Window
}
a(); // error this is undefined
例2: 对象中调用函数
'use strict'
var name = "windowsName";
var a = {
name: "Li Baitian",
fn : function () {
console.log(this.name); // Cherry
}
}
a.fn();
在例2中,调用函数fn的调用者为对象a,所以this指向a,this.name即为对象a的属性name,无论是严格模式还是非严格模式,这种使用方式均不会报错(因为明确指定了函数的调用者)
例3: this只指向最后调用它的对象
var name = "windowsName";
var a = {
name : null,
fn : function () {
console.log(this.name); // windowsName
}
}
var f = a.fn;
f();
call、apply、bind
使用call
const sayMyName = {
name: 'Li Baitian',
print(game) {
console.log(`I am ${this.name}, ${game} is my love`)
}
}
sayMyName.print('lol'); // 'I am Li Baitian, lol is my love'
上面的代码很简单,在对象sayMyName中有print方法,打印了sayMyName对象中的name和一个外部传入的值game,但是,如果我们想在另外一个对象B中使用print方法,来打印对象B中的name和外部传入的game,该如何实现呢?
其实很简单,实现的思路就分两步
-
- 借用
sayMyName中的print方法 - 将
print方法中的this指向B
- 借用
const sayMyName = {
name: 'Li Baitian',
print(game) {
console.log(`I am ${this.name}, ${game} is my love`)
}
}
const B = {
console.log('this----', this);
name: 'lyc'
};
sayMyName.print.call(B, 'Li Baitian'); // 'I am lyc, Li Baitian is my love'
通过上面的输出可以看到,对象B中没有再重新定义一个print方法,而且print方法中的this的确指向的是B
- call方法,可以接受任意多个参数,但是要求,第一个参数必须是待被指向的对象(B),剩下的参数,都传入借过来使用的函数(say)中
手写 call
首先,call方法接受多个参数,第一个参数为借用 函数方法 的对象,剩余参数为函数方法的入参
Function.prototype.myCall = function(context, ...args) {}
接下来,要实现的就是改变this的指向,传入参数。但是我们如何改变this的指向呢?
很简单,我们在需要借用方法的对象上添加一个新的方法,当我们调用这个方法的时候,this就指向这个新的对象了!(注意⚠️ 添加的新方法要避免与target对象上原有的方法名重复,这里使用了ES6的Symbol)
非常关键的一点在于 context[symbolKey] = this ,我们为 借用方法的对象上新添加的方法 赋值了一个this,这个this正是借过来使用的函数,这样我们在执行这个函数的时候,this自然就指向了target。(这里有点不好理解,建议手动实现的时候打打印一下调用函数中的this,查看指向是否有发生了改变)
Function.prototype.myCall = function(context, ...args) {
const symbolKey = Symbol();
context[symbolKey] = this;
}
此外,JS要求在使用call的时候,当target传入一个空对象时,target指向window,所以要多判断下。
Function.prototype.myCall = function(context, ...args) {
context = context || window;
const symbolKey = Symbol();
// 将this指向传进来的对象 context
context[symbolKey] = this;
const res = context[symbolKey](...args);
delete context[symbolKey];
return res;
}
这里拓展一下,针对于ES5的话,可以手写一下ES6的Symbol,这里也当作练习了,下面是手写call的究极完整版 (应该没有bt面试官会问到这里)。
function mySymbol(obj) {
const passWord = (new Date().getTime() + Math.random()).toString(32).slice(0,6);
if (Object.prototype.hasOwnProperty.call(obj, passWord)) {
return mySymbol(obj); // 如果依旧重复了,二二三四再来一次,递归调用
} else {
return passWord;
}
}
Function.prototype.myCall = function(context, ...args) {
context = context || window;
const symbolKey = mySymbol(context);
context[symbolKey] = this;
const result = context[symbolKey](...args);
delete context[symbolKey];
return result;
}
// 执行示例
const sayMyName = {
name: 'Li Baitian',
print(game) {
console.log('this---', this); // { name: 'lyc', [Symbol()]: [Function: print] }
console.log(`I am ${this.name}, ${game} is my love`)
}
}
const B = {
name: 'lyc',
};
sayMyName.print.myCall(B, 'Li Baitian'); // 'I am lyc, Li Baitian is my love'
手写apply
apply与call的区别只是在于,apply传的第二个参数为数组
Function.prototype.myCall = function(context, args) {
context = context || window;
const symbolKey = Symbol();
context[symbolKey] = this;
const result = context[symbolKey](...args);
delete context[symbolKey];
return result;
}
使用bind
bind的使用方式与call和apply没有很大的区别,唯二不同的是:
bind会返回一个绑定好调用对象的函数bind支持在两个地方传入参数:
-
bind绑定时- 调用
bind的返回函数时
const sayMyName = {
name: 'Li Baitian',
print(pre, game) {
console.log(`${pre}, I am ${this.name}, ${game} is my love`)
}
}
sayMyName.print();
const B = {
name: 'lyc'
};
const printB = sayMyName.print.bind(B, 'hi');
printB('Li Baitian'); // I am lyc, Li Baitian is my love
手写bind
类似之前的方式,我们先写一个简易框架
function.prototype.myBind = function(context) {
context = context || {};
return function () {};
}
第二步也没有区别,改变this的指向,由sayMyName改为B 值得注意的是,因为bind返回的是一个偏函数,所以不能在调用之后注销掉context[symbolKey](),会导致后面的调用出现问题
function.prototype.myBind = function(context) {
context = context || {};
const symbolKey = Symbol();
context[symbolKey] = this;
return function () {
context[symbolKey]();
}
};
第三步,将参数传入,不论是bind绑定时传入的,还是调用bind返回的函数时传入的,都传到返回的函数中。
function.prototype.myBind = function(context, outerArgs) {
context = context || {};
const symbolKey = Symbol();
context[symbolKey] = this;
return function (...innerArgs) {
const res = context[symbolKey](...outerArgs, ...innerArgs);
return res;
}
};
总结
具体来说call 、bind 、apply都是用来改变调用函数内部this指向的,关于this指向、三个函数的使用方法和实现方式要多多练习和复习,虽然需要手写的唯一场景就是面试,但是去了解JS内部函数的实现思路和方法,也是通往高级工程师的必经之路,共勉。