实现关键字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;
}