call,apply,bind,new实现原理

8,724 阅读4分钟

在实际开发过程中,对于函数封装时,不确定外部是谁调用的,调用函数内部方法时,有可能是window调用这时就会报错,常使用callapply,bind来绑定this指向。

Function.prototype.call()

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

该方法和apply()类似,区别在于,call()可以接收若干参数,而apply()接收的是一个包含多个参数的数组。

语法:fun.call(thisArg, arg1, arg2, ...)

call 可以继承

通过父类的构造函数call方法实现继承

function Product(name, price) {
    this.name = name;
    this.price = price;
  }
  function Food(name, price) {
    Product.call(this, name, price);
    this.category = 'food';
  }
  var cheese = new Food('feta', 5);
  console.log(cheese)
  // Food { name: 'feta', price: 5, category: 'food' }

实例都会拥有在Product构造函数中添加的name属性和price属性,但category属性是在各自的构造函数中定义的。

call 方法调用匿名函数

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

for循环体内,我们创建了一个匿名函数,然后通过调用该函数的call方法,将每个数组元素作为指定的this值执行了那个匿名函数。

call方法指定上下文的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

Call原理

Function.prototype.myCall = function(context) {
   context = context ? Object(context) : window
   context.fn = this
   let args = [...arguments].slice(1)
   let r = context.fn(args)
   delete context.fn
   return r
}

Function.prototype.apply()

apply()调用一个指定this值的函数, 接收作为一个数组或者类数组对象提供的参数

语法: func.apply(thisArg, [argsArray])

apply 将数组添加到另一个数组

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

apply 找出最大值和最小值

var numbers = [5, 6, 2, 3, 7];
var max = Math.max.apply(null, numbers)
var min = Math.min.apply(null, numbers);

如果参数组非常大,将参数数组切块后,循环传入目标方法:

function minOfArray(arr) {
    var min = Infinity;
    var QUANTUM = 32768;
  
    for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
      var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
      min = Math.min(submin, min);
    }
  
    return min;
  }
  
  var min = minOfArray([5, 6, 2, 3, 7]);
  console.log(min) // 2

apply原理

Function.prototype.myApply = function(context) {
  context = context ? Object(context) : window
    context.fn = this
    let args = [...arguments][1]
    if (!args) {
        return context.fn()
    }
    let r = context.fn(args)
    delete context.fn;
    return r
 }

Function.prototype.bind()

bind()方法创建一个新函数, 在调用时设置this关键字为提供的值。

并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。 语法: function.bind(thisArg, [arg1[, arg2[, ...]]])

创建绑定函数

his.x = 9;    // 在浏览器中,this指向全局的 "window" 对象
var module = {
  x: 81,
  getX: function() { return this.x; }
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX(); // 返回9 - 因为函数是在全局作用域中调用的

var boundGetX = retrieveX.bind(module);  // 创建一个新函数,把 'this' 绑定到 module 对象
boundGetX(); // 81

偏函数

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 ,第二个参数被忽略

fn1.myCall(fn2)时,绑定当前this 需要context.fn = this等价于context.fn = fn1 调用的时候 context.fn() 等价于 fn2.fn()此时thisfn2 并执行fn1

fn1.myCall.myCall(fn2)是此时都是执行myCall函数, thiswindow, 并执行fn2函数。

bind原理


let obj = {
    name: 'joker'
}

function fn() {
    console.log(this.name)
}
Function.prototype.bind = function(context) {

}
let bindFn = fn.bind(obj)
bindFn()
// joker

从上面例子可以看出

  1. bind可以绑定this执行为传入的对象
  2. bind方法返回一个函数(高阶函数) 实现一个简易的bind方法
Function.prototype.bind = function(context) {
   let _me = this
    return function() {
        return _me.apply(context)
    }
}

bind 还可以多次传参 用法:

let obj = {
    name: 'joker'
}

function fn(name, age) {
    console.log(this.name + '今年' + name + age + '岁了')
}
let bindFn = fn.bind(obj, '大概')
bindFn(10)
// joker今年大概10岁了

绑定this的时候传递了一个值, 执行bindFn又传了一个参数,因此之前的函数需要改造

Function.prototype.bind = function(context) {
    let _me = this
    let bindArgs = [].slice.call(arguments, 1) // 获取bind方法传入的参数
    return function() {
        let fnArgs = [].slice.call(arguments) // 获取函数执行传入的参数
        return _me.apply(context, bindArgs.concat(fnArgs))
    }
}

如果当前绑定的函数被new了,当定函数中的this 是当前函数的实例,用法

let obj = {
    name: 'joker'
}
function fn(name, age) {
    console.log(this)  //  this是fn
}
let bindFn = fn.bind(obj)
let instance = new bindFn()

那么这个方法还需要改造一下, 如果当前函数执行中的thisfBound的实例,说明是new执行的,那么当前 this就是函数的实例,否则是context

Function.prototype.bind = function(context) {
    let _me = this
    let bindArgs = [].slice.call(arguments, 1)
    function Fn() {}
    let fBound = function() {
        let fnArgs = [].slice.call(arguments)
        return _me.apply(this instanceof fBound ? this : context, bindArgs.concat(fnArgs))
    }
    Fn.prototype = this.prototype
    fBound.prototype = new Fn();
    return fBound
}

new的原理

想了解new的原理先要了解js的原型机制,先来看张图

let f1 = new Foo()
  1. f1 是构造函数 Foo 的实例, __proto__指向构造函数的原型Foo.prototype
  2. Foo.prototype.constructor指向构造函数Foo, Fooprototype指向它的原型
  3. Foo的原型的__proto__最终指向Object

new的实现

function Animal(type) {
    this.type = type;
}
Animal.prototype.say = function() {
    console.log('say')
}

function mockNew() {
    let Constructor = [].shift.call(arguments); // 取出构造函数
    
    let obj = {}   // new 执行会创建一个新对象
    
    obj.__proto__ = Constructor.prototype 
    
    Constructor.apply(obj, arguments)
    return obj
}
let animal = mockNew(Animal, 'dog')
    
console.log(animal.type) // dog
animal.say() // say