【李拜天的学习笔记】一篇文章彻底讲通this & call & apply & bind

60 阅读5分钟

相信你一定看过很多关于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 因为在严格模式下必须明确指定函数的调用者,如果不明确指定,则thisundefined

'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指向athis.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,该如何实现呢?

其实很简单,实现的思路就分两步

    1. 借用sayMyName中的print方法
    2. 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的话,可以手写一下ES6Symbol,这里也当作练习了,下面是手写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

applycall的区别只是在于,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的使用方式与callapply没有很大的区别,唯二不同的是:

  • 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;
  }
};

总结

具体来说callbindapply都是用来改变调用函数内部this指向的,关于this指向、三个函数的使用方法和实现方式要多多练习和复习,虽然需要手写的唯一场景就是面试,但是去了解JS内部函数的实现思路和方法,也是通往高级工程师的必经之路,共勉。