jannahuang / blog

MIT License
0 stars 0 forks source link

call / apply / bind 的区别 #27

Open jannahuang opened 2 years ago

jannahuang commented 2 years ago

call()

function.call(thisArg, arg1, arg2, ...) thisArg:在 function 函数运行时使用的 this 值。如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。 arg1, arg2, ...:指定的参数列表。 call() 提供新的 this 值给当前调用的函数/方法。

调用父构造函数

function Product(name, price) {
  this.name = name;
  this.price = price;
}
function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}
function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}
var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);

调用匿名函数

var animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

调用函数并且指定上下文的 'this'

function greet() {
  var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
  console.log(reply);
}
var obj = {
  animal: 'cats', sleepDuration: '12 and 16 hours'
};
greet.call(obj);  // cats typically sleep between 12 and 16 hours

调用函数并且不指定第一个参数

在非严格模式下,如果没有传递第一个参数,this 的值将会被绑定为全局对象。在严格模式下,this 的值将会是 undefined。

var sData = 'Wisen';
function display() {
  console.log('sData value is %s ', this.sData);
}
display.call();  // sData value is Wisen

实现 call

使用一个指定的 this 值和一个或多个参数来调用一个函数。

实现要点:

// ES5 写法
Function.prototype.myCall = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1; i < arguments.length; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

// ES6 写法
Function.prototype.myCall = function (context) {
    // 判断调用对象
    if (typeof this !== "function") {
      throw new Error("Type error");
    }
    // 首先获取参数
    let args = [...arguments].slice(1);
    // 判断 context 是否传入,如果没有传就设置为 window
    context = context || window;
    // 将被调用的方法设置为 context 的属性
    // this 即为我们要调用的方法
    context.fn = this;
    // 执行要被调用的方法
    let result = context.fn(...args);
    // 删除手动增加的属性方法
    delete context.fn;
    // 将执行结果返回
    return result;
};

apply()

apply() 函数的语法与 call() 几乎相同,但根本区别在于,call() 接受一个参数列表,而 apply() 接受一个参数的单数组。 apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)形式提供的参数。 apply(thisArg, argsArray) thisArg:在 func 函数运行时使用的 this 值。如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。 argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。 可以使用数组字面量(array literal),如 fun.apply(this, ['eat', 'bananas']),或数组对象,如 fun.apply(this, new Array('eat', 'bananas'))。从 ECMAScript 第 5 版开始,可以使用任何种类的类数组对象,如使用 NodeList 或一个自己定义的类似 {'length': 2, '0': 'eat', '1': 'bananas'} 形式的对象。

用 apply 将数组各项添加到另一个数组

可以使用 push 一次将一个元素或多个元素追加到数组中。可是当 push 的参数是数组,它会将该数组作为单个元素添加。concat 方法可以实现需求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组。使用 apply 则可以将元素追加到现有数组。

const array = ['a', 'b'];
const elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]

内置函数

对于一些需要写循环以遍历数组各项的需求,可以用 apply 完成以避免循环。

// 找出数组中最大/小的数字
const numbers = [5, 6, 2, 3, 7];

// 使用 Math.min/Math.max 以及 apply 函数时的代码
let max = Math.max.apply(null, numbers);
// 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..)
// Math.max() 传值只能展开传,不能传参数 numbers
let min = Math.min.apply(null, numbers);

注意:如果按上面方式调用 apply,有超出 JavaScript 引擎参数长度上限的风险。

链接构造器

Function.prototype.construct = function (aArgs) {
  let oNew = Object.create(this.prototype);
  this.apply(oNew, aArgs);
  return oNew;
};

function MyConstructor() {
  for (let nProp = 0; nProp < arguments.length; nProp++) {
    this['property' + nProp] = arguments[nProp];
  }
}

let myArray = [4, 'Hello world!', false];
let myInstance = MyConstructor.construct(myArray);

console.log(myInstance.property1);                // logs 'Hello world!'
console.log(myInstance instanceof MyConstructor); // logs 'true'
console.log(myInstance.constructor);              // logs 'MyConstructor'

实现 apply

apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。

实现要点:

// ES5 写法
Function.prototype.myApply = function (context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0; i < arr.length; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

// ES6 写法
Function.prototype.myApply = function (context) {
    if (typeof this !== "function") {
      throw new Error("Type error");
    }
    let result = null;
    context = context || window;
    // 与上面代码相比,我们使用 Symbol 来保证属性唯一
    // 也就是保证不会重写用户自己原来定义在 context 中的同名属性
    const fnSymbol = Symbol();
    context[fnSymbol] = this;
    // 执行要被调用的方法
    if (arguments[1]) {
      result = context[fnSymbol](...arguments[1]);
    } else {
      result = context[fnSymbol]();
    }
    delete context[fnSymbol];
    return result;
};

bind()

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 function.bind(thisArg, arg1, arg2, ...) thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。如果使用 new 运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者 thisArg 是null 或 undefined,执行作用域的 this 将被视为新函数的 thisArg。 arg1, arg2, ...:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回值:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

bind() 函数会创建一个新的绑定函数(bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。

创建绑定函数

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

this.x = 9;    // 在浏览器中,this 指向全局的 "window" 对象
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 81

var retrieveX = module.getX;
retrieveX();
// 返回 9 - 因为函数是在全局作用域中调用的

// 创建一个新函数,把 'this' 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81

偏函数

bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。

function list() {
  return Array.prototype.slice.call(arguments);
}
function addArguments(arg1, arg2) {
    return arg1 + arg2
}

var list1 = list(1, 2, 3); // [1, 2, 3]
var result1 = addArguments(1, 2); // 3

// 创建一个函数,它拥有预设参数列表。
var leadingThirtysevenList = list.bind(null, 37);
// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var list2 = leadingThirtysevenList();
// [37]
var list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]
var result2 = addThirtySeven(5);
// 37 + 5 = 42
var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二个参数被忽略

配合 setTimeout

在默认情况下,使用 window.setTimeout() 时,this 关键字会指向 window(或 global)对象。当类的方法中需要 this 指向类的实例时,显式地把 this 绑定到回调函数,就不会丢失该实例的引用。

function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 在 1 秒钟后声明 bloom
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function() {
  console.log('I am a beautiful flower with ' +
    this.petalCount + ' petals!');
};

var flower = new LateBloomer();
flower.bloom();  // 一秒钟后,调用 'declare' 方法

实现 bind

bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

实现要点:

Function.prototype.myBind = function (context) {
    // 调用 bind 的不是函数,需要抛出异常
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    // this 指向调用者
    var self = this;
    // 实现第2点,因为第1个参数是指定的this,所以只截取第1个之后的参数
    var args = Array.prototype.slice.call(arguments, 1);

    // 创建一个空对象
    var fNOP = function () {};

    // 实现第3点,返回一个函数
    var fBound = function () {
        // 实现第4点,获取 bind 返回函数的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        // 然后同传入参数合并成一个参数数组,并作为 self.apply() 的第二个参数
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
        // 注释1
    }

    // 注释2
    // 空对象的原型指向绑定函数的原型
    fNOP.prototype = this.prototype;
    // 空对象的实例赋值给 fBound.prototype
    fBound.prototype = new fNOP();
    return fBound;
}

注释1 :

注释2 :

区别

  1. call() 和 apply() 的语法区别在于call() 接受一个参数列表,而 apply() 接受一个参数的单数组。apply() 语法跟 call() 一样。
  2. call() 和 apply() 立即执行,而 bind() 是返回一个新的函数,调用后执行。
    
    var module = {
    x: 81,
    getX: function() { return this.x; }
    };

module.getX.call(module) // 81 module.getX.apply(module) // 81 let newFn = module.getX.bind(module) newFn() // 81



参考:
[解析 bind 原理,并手写 bind 实现](https://github.com/sisterAn/JavaScript-Algorithms/issues/81)
[死磕 36 个 JS 手写题](https://juejin.cn/post/6946022649768181774)