实现关键字apply,call,bind

call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。 因为在js中函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

首先我们看看这句话怎么理解。

function Animal() {}
Animal.prototype = {
    voice: "汪汪",
    say: function() {
        console.log(this.voice);
    }
}
 
let dog = new Animal();
dog.say();    //汪汪

如果我们有一个对象cat= {voice : "喵"} ,我们不想对它重新定义 say 方法, 那么我们可以通过 call 或 apply 用 Animal 的 say 方法:

cat = {
    voice: "喵"
}
dog.say.call(cat);     //喵
dog.say.apply(cat);    //喵

可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法,但是其他的有,我们可以借助call或apply用其它对象的方法来操作。

# apply、call 的区别

作用完全一样,只是接受参数的方式不太一样.

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])

其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,
而 apply 则是把参数放在数组里。

js中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call 。 而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个伪数组来遍历所有的参数。

# 一些常用用法

获取数组最大最小值,数组本身并没有取最大最小值点额方法,但是Math有

const nums = [1,3,4,5,6,7]
const max = Math.max.apply(Math,nums)// '7'
const min = Math.max.apply(Math,nums)// '1'

判断类型,传入任意对象,调用toString方法,就可以获取到该对象到类型。

function getType(obj) {
  return Object.prototype.toString.call(obj)
}

/**
    Object.prototype.toString.call(null)
    "[object Null]"
    Object.prototype.toString.call()
    "[object Undefined]"
    Object.prototype.toString.call([])
    "[object Array]"
*/

类数组使用数组方法

var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));

js中有一些类数组的对象结构。比如 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回Nod 它们返回NodeList对象都属于类数组。
具体表现就是他们不能应用 Array下的 push , pop 等方法。

我们可以通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。

我们尝试自己实现下。

# call

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

语法

function.call(thisArg, arg1, arg2, ...)

# 拆解要点

如何模拟实现 call

  • 所有函数都可以调用,说明call是原型方法- Function.prototype.call
  • 如果第一个参数没有传入,那么默认指向 window / global(非严格模式)
  • 传入 call 的第一个参数是 this 指向的对象,根据隐式绑定的规则 obj.foo(), foo() 中的 this 指向 obj;因此我们可以这样调用函数 thisArgs.func(...args)
  • 返回执行结果
Function.prototype.call1 = function() {
	let [thisArg,...args] = [...arguments] // 结构参数
	if(!thisArg){ // 第一个参数为this指向的对象
		thisArg = window||global
	}
    thisArg.fn = this;  // 暂存
    let result = thisArg.fn(...args); // 调用
    delete thisArg.fn;
    return result;
}
// 实现2
Function.prototype.call2 = function(context = window) {
	if(!context)context=window||global // 可能传入null
    context.fn = this;  
    let args = [...arguments].slice(1);
    let result = context.fn(...args);
    delete context.fn;
    return result;
}

# apply

apply 与call类似,只是参数方式不一样,call 是传入不固定个数的参数,而 apply 是传入一个数组;函数内部需要处理下

Function.prototype.apply1 = function(context = window) {
	if(!context) context=window||global
    context.fn = this
    let result;
    // 判断是否有第二个参数
    if(arguments[1]) {
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    delete context.fn
    return result
}

const max = Math.max.apply1(Math,nums)// '7'

# bind

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

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() 除了 this 外,还可传入多个参数;
  • bing 创建的新函数可能传入多个参数;
  • 新函数可能被当做构造函数调用;
  • 函数可能有返回值;
Function.prototype.bind2 = function (context) {
    const self = this;
    const args = Array.prototype.slice.call(arguments, 1);

    const fNOP = function () {};

    const fBound = function () {
        const bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

# 参考链接

call (opens new window) bind (opens new window)

最近更新
01
echarts扇形模拟镜头焦距与可视化角度示意图
03-10
02
vite插件钩子
03-02
03
vite的依赖预构建
02-13
更多文章>