JavaScript中的apply、call和bind

129 阅读5分钟

call,apply,bind的基本介绍

语法

fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)

参数

thisArg(可选):

  • fun的this指向thisArg对象
  • 非严格模式下:thisArg指定为null,undefined,fun中的this指向window对象.
  • 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean

param1,param2(可选): 传给fun的参数。

  • 如果param不传或为 null/undefined,则表示不需要传入任何参数.
  • apply第二个参数为数组,数组内的值为传给fun的参数。

调用call/apply/bind的必须是个函数

call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。

只要是函数就可以,比如:Object.prototype.toString就是个函数,我们经常看到这样的用法:Object.prototype.toString.call(data)。

作用

改变函数执行时的this指向,目前所有关于它们的运用,都是基于这一点来进行的。

返回值

  • call/apply 返回fun的执行结果
  • bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数

call()和apply()

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

call()和apply()的区别在于:call()方法接受的是若干个参数列表,而apply()方法接受的是一个包含多个参数的数组。

var func = function(arg1, arg2) {
     ...
};

func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组

使用场景

1. 判断数据类型

function isType(data, type) {
    const typeObj = {
        '[object String]': 'string',
        '[object Number]': 'number',
        '[object Boolean]': 'boolean',
        '[object Null]': 'null',
        '[object Undefined]': 'undefined',
        '[object Object]': 'object',
        '[object Array]': 'array',
        '[object Function]': 'function',
        '[object Date]': 'date', // Object.prototype.toString.call(new Date())
        '[object RegExp]': 'regExp',
        '[object Map]': 'map',
        '[object Set]': 'set',
        '[object HTMLDivElement]': 'dom', // document.querySelector('#app')
        '[object WeakMap]': 'weakMap',
        '[object Window]': 'window',  // Object.prototype.toString.call(window)
        '[object Error]': 'error', // new Error('1')
        '[object Arguments]': 'arguments',
    }
    let name = Object.prototype.toString.call(data) // 借用Object.prototype.toString()获取数据类型
    let typeName = typeObj[name] || '未知类型' // 匹配数据类型
    return typeName === type // 判断该数据类型是否为传入的类型
}
console.log(
    isType({}, 'object'), // true
    isType([], 'array'), // true
    isType(new Date(), 'object'), // false
    isType(new Date(), 'date'), // true
)

2. 类数组对象(Array-like Object)使用数组方法

var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function

var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1"); // 505 不同环境下数据不同
// (505) ["h1", html.gr__hujiang_com, head, meta, ...] 

类数组对象有下面两个特性:

  • 具有:指向对象元素的数字索引下标和 length 属性
  • 不具有:比如 push 、shift、 forEach 以及 indexOf 等数组对象具有的方法

要说明的是,类数组对象是一个对象。JS中存在一种名为类数组的对象结构,比如 arguments 对象,还有DOM API 返回的 NodeList 对象都属于类数组对象,类数组对象不能使用 push/pop/shift/unshift 等数组方法,通过 Array.prototype.slice.call 转换成真正的数组,就可以使用 Array下所有方法。

类数组对象转数组的其他方法:

// 上面代码等同于
var arr = [].slice.call(arguments);

ES6:
// Array.from() 可以将两类对象转为真正的数组:类数组对象和可遍历(iterable)对象(包括ES6新增的数据结构 Set 和 Map)。
let arr = Array.from(arguments);
let arr = [...arguments];

PS扩展一:为什么通过 Array.prototype.slice.call() 就可以把类数组对象转换成数组?

下面代码是 MDN 关于 slice 的Polyfill

Array.prototype.slice = function(begin, end) {
      end = (typeof end !== 'undefined') ? end : this.length;

      // For array like object we handle it ourselves.
      var i, cloned = [],
        size, len = this.length;

      // Handle negative value for "begin"
      var start = begin || 0;
      start = (start >= 0) ? start : Math.max(0, len + start);

      // Handle negative value for "end"
      var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
      if (end < 0) {
        upTo = len + end;
      }

      // Actual expected size of the slice
      size = upTo - start;

      if (size > 0) {
        cloned = new Array(size);
        if (this.charAt) {
          for (i = 0; i < size; i++) {
            cloned[i] = this.charAt(start + i);
          }
        } else {
          for (i = 0; i < size; i++) {
            cloned[i] = this[start + i];
          }
        }
      }

      return cloned;
    };
  }

PS扩展二:通过 Array.prototype.slice.call() 就足够了吗?存在什么问题?

在低版本IE下不支持通过Array.prototype.slice.call(args)将类数组对象转换成数组,因为低版本IE(IE < 9)下的DOM对象是以 com 对象的形式实现的,js对象与 com 对象不能进行转换。

function toArray(nodes){
    try {
        // works in every browser except IE
        return Array.prototype.slice.call(nodes);
    } catch(err) {
        // Fails in IE < 9
        var arr = [],
            length = nodes.length;
        for(var i = 0; i < length; i++){
            // arr.push(nodes[i]); // 两种都可以
            arr[i] = nodes[i];
        }
        return arr;
    }
}

3. 获取数组中的最大值和最小值

var numbers = [5, 458 , 120 , -215 ]; 
Math.max.apply(Math, numbers);   //458    
Math.max.call(Math, 5, 458 , 120 , -215); //458

// ES6
Math.max.call(Math, ...numbers); // 458
Math.max(...numbers); //458

4. 调用父构造函数实现继承

function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代码,继承自SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]

var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]

在子构造函数中,通过调用父构造函数的call方法来实现继承,于是SubType的每个实例都会将SuperType 中的属性复制一份。

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

bind()

bind()方法会创建一个新函数,当这个新函数被调用时,它的this值时传递给bind()的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

语法:fun.bind(thisArg, [, arg1[, arg2[, ...]]])

bind方法与call/apply最大的不同就是前者返回一个绑定上下文的函数,而后者是直接执行了函数。

例子说明:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

var bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}

通过上述代码可以看出bind️有如下特性:

  • 可以指定this
  • 返回一个函数
  • 可以传入参数
  • 柯里化

使用场景

1.业务场景

经常有如下的业务场景

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty

这里输出的nickname是全局的,并不是我们创建person时传入的参数,因为setTimeout在全局环境中执行,所以this指向的是window。 这里把setTimeout换成异步回调也是一样的,比如接口请求回调。

解决方案如下:

解决方案1: 缓存this值

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
        
		var self = this; // 缓存this值
        setTimeout(function(){
            console.log("Hello, my name is " + self.nickname); // changed
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

解决方案2: 使用bind

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }.bind(this), 500); // 使用bind
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

2. 柯里化(curry)

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

可以一次性地调用柯里化函数,也可以每次只传一个参数分多次调用。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

上述代码定义了一个add函数,它接受一夜参数并返回一个新的函数。调用add之后,返回的函数就通过闭包的方式记住了add的第一个参数。所以bind本身也是闭包的一种使用场景。

题目:

用 JS 实现一个无限累加的函数 add,示例如下

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10 

// 以此类推
function add(a){
    function sum(b){
        a = a + b;
        return sum;
    }
    sum.toString = function(){
        return a;
    }
    return sum;
}

参考:

github.com/yygmind/blo… juejin.cn/post/684490…