JavaScript中的call、apply、bind的概念和应用?

203 阅读8分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」。

哈喽,我是刘十一,今天我们一起来看一看JavaScript中的call、apply、bind的概念和具体用法。在之前 这篇文章提到过有关于this的各种情况,其中有一种情况就是通过call、apply、bind来将this绑定到指定的对象上,即,这三个方法都可以改变函数体内部this的指向。

那么这三个方法又有什么区别呢?它们分别适合应用在哪些场景中呢?

在介绍它们之前让我们先来看一个例子吧~~~

var person = {
  name: "Niu Xiaoliu",
  age: 23
};
function say(job){
  console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"Coder"); // Niu Xiaoliu:23 Coder
say.apply(person,["Coder"]); // Niu Xiaoliu:23 Coder
var sayPerson = say.bind(person,"Coder");
sayPerson(); // Niu Xiaoliu:23 Coder

解析: 对于 对象person 而言,并没有say方法,通过call/apply/bind就可以将外部的say方法应用于这个对象中,其实就是将say内部的this 指向 person这个对象。

一、call

1、概念

call 是属于所有Function的方法,也就是Function.prototype.call。

call() 方法调用一个函数, 其具有一个指定的this值 和 分别地提供的参数(参数的列表)。

语法:

fun.call(thisArg[,arg1[,arg2,…]]);

参数:

  • 其中,thisArg 就是this指向,arg是指定的参数(即,在外面传入的thisArg值 会修改并成为this值)。

  • call方法的length属性值为1 Function.prototype.call.length = 1

  • thisArg 是 undefined或null时 它会被替换成 全局对象,所有其他值会被应用到Object并将结果作为this值(这是第三版引入的更改)。

call的用处:

call的用处简而言之就是,可以让call()中的对象调用当前对象所拥有的function。

2、ECMAScript规范中定义的call

当以 thisArg 和 可选的arg1,arg2等等 作为参数 在一个func对象上 调用call方法,采用如下步骤

(1)如果IsCallable(func) (可调用的函数)是false, 则抛出一个TypeError异常。

(2)令 argList(参数列表)为一个空列表。

(3)如果调用这个方法的 参数多余一个,则从arg1开始以 从左到右的顺序 将每个参数插入至argList的尾部。

(4)提供thisArg作为this值并以argList作为参数列表,调用func的[[Call]]内部方法,返回结果。

3、使用call调用函数并且指定this

var obj = {
  a: 1
}
function foo(b, c){
  this.b = b;
  this.c = c;
  console.log(this.a + this.b + this.c);
}

foo.call(obj,2,3); // 6

4、call实现继承

在 需要实现继承的 子类构造函数中,可以 通过call 调用父类构造函数 实现继承。

function Person(name, age){
  this.name = name;
  this.age = age;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
function Student(name, age, job){
  Person.call(this, name ,age);
  this.job = job;
  this.say = function(){
    console.log(this.name + ":" + this.age + " " + this.job);
  }
}
var me = new Student("Niu Xiaoliu",23,"Coder");

二、apply

1、概念

apply 也是属于所有Function的方法,也就是Function.prototype.apply。

apply() 方法调用一个函数, 其具有 一个指定的this值,以及 作为一个数组(或类似数组的对象)提供的参数。

语法:

fun.apply(thisArg, [argsArray]);

参数:

  • 其中,thisArg 就是this指向(即,在外面传入的thisArg值 会修改并成为this值),argsArray是指定的 参数数组。

  • apply方法的length属性值为2 Function.prototype.apply.length = 2

  • thisArg是undefined或null时它会被替换成 全局对象,所有其他值会 被应用到Object 并将结果 作为this值(第三版引入的更改)。

apply的用处:

在用法上apply和call一样,可以让apply()中的对象调用当前对象所拥有的function。

从语法上看,call和apply的在参数上的一个区别:

(1)call 的参数是 一个列表,将每个参数一个个列出来

(2)apply 的参数是 一个数组,将每个参数放到一个数组中

2、ECMAScript规范中定义的apply

当以thisArg 和 argArray为参数 在一个func对象上调用apply方法,采用如下步骤

(1)如果IsCallable(func) (可调用的函数)是false, 则抛出一个TypeError异常。

(2)如果argArray(参数数组)是null或undefined, 则返回 提供thisArg作为this值 并以 空参数列表 调用func的[[Call]]内部方法的结果。

(3)如果Type(argArray)不是Object, 则抛出一个TypeError异常。

(4)令 len 为 以"length"作为参数 调用argArray的[[Get]]内部方法的结果。

(5)令 n 为 ToUint32(len)。

(6)令 argList 为 一个空列表。

(7)令 index 为 0。

(8)只要 index < n 就重复。

(9)令 indexName 为 ToString(index)。

(10)令 nextArg 为 以indexName 作为参数 调用argArray的[[Get]]内部方法的结果。

(11)将 nextArg 作为 最后一个元素 插入到 argList里。

(12)设定 index 为 index + 1。

(13)提供 thisArg 作为 this值 并以 argList 作为 参数列表,调用func的[[Call]]内部方法,返回结果。

3、实现一个apply

(1)绑定上下文

成功地将this指向了me对象,而不是本身的obj对象

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取
  // (this指向 getName() 方法,context 为传入的第一个参数,即指向的对象 me)
  context.fn = this;
  // 执行这个函数
  context.fn();
  // 从上下文中删除函数引用
  delete context.fn;
}

var obj ={
  name: "xl",
  getName: function(){
    console.log(this.name);
  }
}

var me = {
  name: "Niu Xiaoliu"
}

obj.getName(); // xl 
obj.getName.myApply(me); // Niu Xiaoliu
(2)给定参数

上文已经提到apply需要接受一个参数数组,可以是一个类数组对象,获取函数参数可以用arguments。

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取
  context.fn = this;
  // 通过arguments获取参数
  var args = arguments[1];
  // 执行这个函数,用ES6的...运算符将arg展开
  context.fn(...args);
  // 从上下文中删除函数引用
  delete context.fn;
}

var obj ={
  name: "xl",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var me = {
  name: "Niu Xiaoliu"
}

obj.getName(); // xl:undefined
obj.getName.myApply(me,[23]); // Niu Xiaoliu:23
(3)当传入apply的this为null或者为空时

我们知道,当apply的第一个参数,也就是this的指向 为null时,this会指向window。知道了这个,再实现就比较简单啦~

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取,如果context不存在,则为window
  var context = context || window;
  context.fn = this;
  //获取传入的数组参数
  var args = arguments[1];
  if (args == undefined) { //没有传入参数直接执行
    // 执行这个函数
    context.fn()
  } else {
    // 执行这个函数
    context.fn(...args);
  }
  // 从上下文中删除函数引用
  delete context.fn;
}

var obj ={
  name: "xl",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var name = "window.name";

var me = {
  name: "Niu Xiaoliu"
}

obj.getName(); // xl:23
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [23]); // window.name:23
obj.getName.myApply(me, [23]); // Niu Xiaoliu:23
(4)保证fn函数的唯一性

ES6 中新增了一种 基础数据类型 Symbol。

const name = Symbol();
const age = Symbol();
console.log(name === age); // false

const obj = {
  [name]: "Niu Xiaoliu",
  [age]: 23
}

console.log(obj); // {Symbol(): "Niu Xiaoliu", Symbol(): 23}
console.log(obj[name]); // Niu Xiaoliu

综上,我们就可以通过Symbol来创建一个唯一的属性名。

var fn = Symbol();
context[fn] = this;
(5)完整的apply
Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取,如果context不存在,则为window
  var context = context || window;
  var fn = Symbol();
  context[fn] = this;
  //获取传入的数组参数
  var args = arguments[1];
  if (args == undefined) { //没有传入参数直接执行
    // 执行这个函数
    context[fn]()
  } else {
    // 执行这个函数
    context[fn](...args);
  }
  // 从上下文中删除函数引用
  delete context.fn;
}

测试

var obj ={
  name: "xl",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var name = "window.name";

var me = {
  name: "Niu Xiaoliu"
}

obj.getName(); // xl:25
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [2]); // window.name:23
obj.getName.myApply(me, [23]); // Niu Xiaoliu:23

三、bind

1、概念

bind()方法 创建一个 新的函数, 当被调用时,将其this关键字 设置为 提供的值,在调用新函数时,在任何提供之前 提供一 个给定的 参数序列。

语法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

参数:

  • 其中,thisArg就是this指向,arg是指定的参数。

  • 可以看出,bind会创建一个新函数(称之为绑定函数),原函数的一个拷贝,也就是说不会像call和apply那样立即执行

  • 当这个绑定函数被调用时,它的this值传递给bind的一个参数,执行的参数 是 传入bind的 其它参数 和 执行绑定函数时 传入的参数。

2、bind(绑定函数)的用法

(1)bind() 最简单的用法 是创建一个函数,使这个函数不论怎么调用都有同样的this值。

(2)常见的错误就是将方法从对象中拿出来,然后调用,并且希望this指向原来的对象。

(3)如果不做特殊处理,一般会丢失原来的对象。

使用bind()方法能够很方便的解决上述的问题:

this.num = 80; 
var mymodule = {
  num: 100,
  getNum: function() { 
    console.log(this.num);
  }
};

mymodule.getNum(); // 100

var getNum = mymodule.getNum;

getNum(); // 80, 因为在这个例子中,"this"指向全局对象

var boundGetNum = getNum.bind(mymodule);
boundGetNum(); // 100

3、bind的用法

function Person(name){
  this.name = name;
  this.say = function(){
    setTimeout(function(){
      console.log("hello " + this.name);
    },1000)
  }
}
var person = new Person("Niu Xiaoliu");
person.say(); //hello undefined

当我们执行上面的代码时,我们希望可以正确地输出name,但是,这里this运行时是指向的window,所以this.name是undefined。

原因:

  • 由 setTimeout() 调用的代码 运行在 与所在函数 完全分离的 执行环境上。

  • 这会导致,这些代码中 包含的 this 关键字 在非严格模式 会指向 window。

解决方案:

方案一

function Person(name){
  this.name = name;
  this.say = function(){
    var self = this; //存储起来 
    setTimeout(function(){
      console.log("hello " + self.name);
    },1000)
  }
}
var person = new Person("Niu Xiaoliu");
person.say(); //hello Niu Xiaoliu

方案二

function Person(name){
  this.name = name;
  this.say = function(){
    setTimeout(function(){
      console.log("hello " + this.name);
    }.bind(this),1000)
  }
}
var person = new Person("Niu Xiaoliu");
person.say(); //hello Niu Xiaoliu

四、总结

(1)三者都是用来改变函数的this指向

(2)三者的第一个参数都是this指向的对象

(3)bind是返回一个绑定函数 可稍后执行,call、apply是 立即调用

(4)三者都可以给定参数传递

(5)call给定参数需要将参数全部列出,apply给定参数数组