Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

call / apply / bind 的区别 #27

Open
jannahuang opened this issue Sep 28, 2022 · 0 comments
Open

call / apply / bind 的区别 #27

jannahuang opened this issue Sep 28, 2022 · 0 comments

Comments

@jannahuang
Copy link
Owner

jannahuang commented Sep 28, 2022

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 值和一个或多个参数来调用一个函数。

实现要点:

  • this 可能传入 null;
  • 传入不固定个数的参数;
  • 函数可能有返回值;
// 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 是传入一个数组。

实现要点:

  • this 可能传入 null;
  • 传入一个数组;
  • 函数可能有返回值;
// 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() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

实现要点:

  • 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 :

  • 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true ,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。
  • 当作为普通函数时,this 指向 window ,此时结果为 false ,将绑定函数的 this 指向 context

注释2 :

  • 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj 可以获取到 bar 原型上的 friend
  • 至于为什么使用一个空对象 fNOP 作为中介,把 fBound.prototype 赋值为空对象的实例(原型式继承),这是因为直接 fBound.prototype = this.prototype 有一个缺点,修改 fBound.prototype 的时候,也会直接修改 this.prototype ;其实也可以直接使用ES5的 Object.create() 方法生成一个新对象,但 bind 和 Object.create() 都是ES5方法,部分IE浏览器(IE < 9)并不支持。

区别

  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 实现
死磕 36 个 JS 手写题

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant