「JavaScript进阶」一文吃透call、apply、bind、new

1,406 阅读7分钟

现在前端一般直接使用的Reactvue等框架来开发,往往忽视了对 JavaScript 基础的掌握,而基础是否扎实,是衡量一个开发技术水平很重要的指标,所以大厂面试往往会考查 JavaScript 基础

花了半年时间针对一些大厂面试常见的JavaScript 基础做一次系统的梳理:JavaScript进阶。觉得对你有帮助的,别忘了点个赞哦~


在阅读本文之前,如果对this绑定还不是很理解,建议先去看看我的另一文章--深入浅出this绑定

call 和 apply

callapply放在一起,是因为这两个方法非常接近,区别在于调用的时候参数形式不同。

call 和 apply 的原理及区别

call()接受的是多个对象作为参数,而apply()接受的是多个对象组成的数组作为参数

直接看代码直观点,定义一个函数person,分别用call()apply() 调用,我们来看下传递的参数的区别:

var func = function(name, age) {
     console.log(`my name is ${name}, my age is ${age}`)
};

func('谷底飞龙', 28); // 直接调用方法
func.call(this, '谷底飞龙', 28); // call执行方法,参数是多个对象
func.apply(this, ['谷底飞龙', 28]) // apply执行方法,参数是多个对象组成的数组

使用上述三种方式执行方法,都能打印出my name is 谷底飞龙, my age is 28

小结:从上述案例,可以看出,使用call()apply()调用的时候,主要有两方面特性:

1、通过第一个参数来改变方法内部的this绑定`(可以参看this绑定之显示绑定

2、剩余的参数会解析成原函数的参数(注:apply()中的数组会被解构成一系列对象)。

call 和 apply 使用方法及场景

从上面小结中,我们知道call和apply主要有两方面的特性,使用场景就是基于这两方面可以分为两类:

1.改变this指向

从上面小结中,我们知道call和apply的第一个特性通过第一个参数来改变函数内部的this指向,利用这一特性的使用场景比较多。

1.1 解决隐式绑定丢失问题

我们来把this绑定之箭头函数绑定中的案例回顾下:

var name = "天下无敌";
var Person = {
  name : "谷底飞龙",
  getName: function () {
    console.log(`my name is ${this.name}`)
  },
  consoleName: function () {
    // 回调函数不使用箭头函数,属于隐式绑定中的隐式丢失的情况,this绑定的是全局对象window
    setTimeout(function (){
        this.getName()
    },100);
  }
};

// 执行函数
Person.consoleName()  

这种情况下,执行方法Person.consoleName()会报错Uncaught TypeError: this.getName is not a function,除了把setTimeout()的回调函数改用箭头函数外,还可以使用call和apply来改变this指向的方式解决。如下

var name = "天下无敌";
var Person = {
  name : "谷底飞龙",
  getName: function () {
    console.log(`my name is ${this.name}`)
  },
  consoleName: function () {
    // 回调函数不使用箭头函数,使用call/apply硬绑定到Person对象
    setTimeout(function (){
        this.getName()
    }.call(Person),100);
  }
};
// 执行函数
Person.consoleName()  

通过setTimeout()调用call(),将回调函数中的this硬绑定到对象Person,执行函数后,就能打印出my name is 谷底飞龙。这里把call换成bind也可以,具体详见本文后面对bind的介绍。

1.2 合并两个数组
const arr1 = [1,2,3];
const arr2 = [4,5,6];
// 使用apply()合并数组,合并后的 arr1 为 [1、2、3、4、5、6]
Array.prototype.push.apply(arr1,arr2)

使用Array.prototype.push.apply(arr1,arr2)时,apply()将第一个参数arr1硬绑定到Array内部的this,因此这里等价于arr1.push(arr2)

  • 注:由于浏览器对函数的参数个数限制(JS核心限制在 65535),如果数组太长,不建议使用apply的方式,在不同浏览器可以会出现数据丢失或者报错。可以将参数数组分成多组,循环遍历执行push.apply()来解决问题(但是遍历会损耗性能,不建议使用)
1.3 类型判断

typeof也可以进行类型判断的,但是typeof并不能准确判断一个对象类型,比如nullArray 的结果都是 object。而使用Object.prototype.toString.call(obj)就能精确的进行类型判断。

咱们先来看看不同类型打印出来的日志

toString = Object.prototype.toString;
 
console.log(toString.call(['谷底飞龙'])); //[object Array]
console.log(toString.call('谷底飞龙')); //[object String]
console.log(toString.call({name: '谷底飞龙'})); //[object Object]
console.log(toString.call(/谷底飞龙/)); //[object RegExp]
console.log(toString.call(123)); //[object Number]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]

通过Object.prototype.toString.call(obj),输出的是[object xxx]字符串,可以利用这一特点进行类型的精确判断,这里的call的作用就是将toString()内部的this指向obj

延伸:判断类型的时候,那为什么不直接用obj.toString()呢?

我们先来运行下代码:

console.log(['谷底飞龙','天下无敌'].toString()); //谷底飞龙、天下无敌
console.log('谷底飞龙'.toString()); //谷底飞龙

直接调用obj.toString()返回的是obj对应的字符串,而不是对象类型。因为toString()是Object的实例方法,而obj(如Array、Function等类型)是Object的实例,实例都对toString()进行了重写(注:重写后,Functionl类型返回的是字符串,Array类型返回的是数组元素组成的字符串),根据原型链知识,调用obj.toString(),执行的是重写后的方法,只能获取到对应的字符串,不能获取到对象类型。因此,应该通过原型方法Object.prototype.toString去判断对象类型。

  • 判断是否是数组,可以这样写:
function isArray(obj) {
   return Object.prototype.toString.call(obj) === '[object Array]';
}

2.解构参数数组

利用第二个特性调用apply()时,会将剩余参数数组解构成一系列对象,我们可以利用这一特性对数组进行一些操作。

2.1 获取数组中的最大值和最小值
const arr = [88,78,128,89,45]
// 使用apply解构数组参数
Math.max.apply(null,arr)

// ES6中解构方法
Math.max(...arr)
// ES6中使用call
Math.max.call(null,...arr)
  • Math.maxMath.min本身是不能对数组直接计算的,可以把数组解构成一系列对象再操作。apply()对第二个参数arr的解构,类似ES6中解构方法...arr,可以把数组解构成一系列对象。
  • 由于这里的this的绑定没影响,所以apply()call()的第一个参数可以随便设置,这里设置为null

call 和 apply 的模拟实现

apply 的模拟实现

Function.prototype.myApply = function (context) {
  context = context || window;
  let fn = Symbol(); // 采用 ES6 的 Sybmol()独一无二的特点,解决 fn 同名覆盖问题
  // 1.将函数挂载到传入的对象
  context[fn] = this; 
  let result;
  // 2.执行对象的方法
  if (arguments[1]) {
    result = context[fn](...arguments[1]);
  } else {
    result = context[fn]();
  }
  // 3.移除对象的方法
  delete context[fn];
  return result;
};

call实现:与apply的唯一区别就是参数格式不同

Function.prototype.myCall = function(context, ...args) {
  context = context || window;
  let fn = Symbol();
  context[fn] = this; // 1.将函数挂载到传入的对象
  let result = context[fn](...args); // 2.执行对象的方法 
  delete context[fn]; // 3.移除对象的方法
  return result;
}

bind

bind 的原理及与 call/apply 的区别

bind使用与callaplly很相似,调用的时候,第一个参数用于修改函数内部this的指向,从第二个参数开始是传递给函数的数据参数。与callapply的最大的不同点就是bind会返回一个新函数,而call和apply只执行函数。 我们来直接看个案例会更直观点

var name = '天下无敌'
var Person = {
  name: '谷底飞龙',
}
var getName = function () {
  console.log(`my name is ${this.name}`)
}
// 使用bind硬绑定到Person对象
const consoleName = getName.bind(Person)
// 执行函数
consoleName()

通过bind将方法getName内部的this指向Person对象,并返回一个新函数consoleName,执行新函数consoleName()后,会打印出Person对象内部的name,也就是my name is 谷底飞龙

  • 1、如果不调用bind,直接执行getName(),打印的将是全局window的name,也就是my name is 天下无敌
  • 2、如果将这里的bind改成call/apply,由于call/apply只执行函数,不会返回新函数,执行函数就会报错Uncaught TypeError: consoleName is not a function

小结:综上,可以看出,bind主要有三个特性(前两个与call/apply相同)

1、通过第一个参数来改变方法内部的this绑定`(可以参看this绑定之显示绑定

2、剩余的参数会解析成原函数的参数

3、返回一个新函数

bind 使用方法及场景

利用小结中bind的三个特性,我们可以知道bind的使用方法场景跟call/apply差不多,比如前面提到的通过改变this的指向来解决this隐式绑定丢失的问题,这里就不赘述了。主要讲一下利用返回一个新函数的特性的使用场景。

bind 的模拟实现

正在补充中

new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 ——(来自于MDN)

new 的原理与分析

new绑定中,我更通俗的解释了下:用 new 调用函数时,会创建一个新实例对象,函数里this绑定的就是该新对象,该实例对象可以访问函数的属性和方法

function Person(name){
  // 属性
  this.name = name;
}
// person的方法
Person.prototype.getAge = function() {
  return 18;
}
// 通过new调用函数创建对象实例
var P = new Person('谷底飞龙')
console.log(`my name is ${P.name}, my age is ${P.getAge()}`)

打印出my name is 谷底飞龙, my age is 18,我们可以得出new调用函数创建的实例对象有两个特性:

1、该实例可以访问函数的属性

2、该实例可以访问函数的方法

new 的模拟实现

function myNew(context) {
  const obj = new Object();
  obj.__proto__ = context.prototype;
  const res = context.apply(obj, [...arguments].slice(1));
  return typeof res === "object" ? res : obj;
}

觉得对你有帮助的,别忘了点个赞哦~

对JavaScript基础进阶进行的系列解读:

参考文档:


结语

如果你也是一个对投资理财感兴趣的程序员,欢迎关注我的公众号「谷底飞龙」,一起成为技术界的投资大佬吧。