# 1. 前言
使用原生JS实现call和apply函数,充分了解其内部原理。call和apply都是为了解决改变this的指向。作用都相同,只是传参的方式不同。除了第一个参数外,call可以接受一个参数列表,apply只接受一个参数数组。
# 2. call 函数
# 2.1 描述
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
# 2.2 语法
fun.call(thisArg, arg1, arg2, ...)
# 2.3 参数
- thisArg:可选的。在 fun 函数运行时指定的
this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数在非严格模式(opens new window)下运行,则指定为null和undefined的this值会自动指向全局对象(浏览器中就是 window 对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。如果处于严格模式下,第一个参数是谁,this 就指向谁,包括 null 和 undefined,如果不传参数 this 就是 undefined - arg1, arg2, ...:可选的。指定的参数列表。
# 2.4 返回值
使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
# 2.5 实现
Function.prototype.myCall = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
if (context === null || context === undefined) {
context = window; // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
} else {
context = Object(context); // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
context.fn = this;
//通过参数伪数组将context后面的参数取出来
let args = [...arguments].slice(1);
let result = context.fn(...args);
//删除 fn
delete context.fn;
return result;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
实现思路:
- 首先,判断调用
mycall的是不是函数,如果不是,则直接抛出异常; - 接着,判断是否传入了第一个参数
context,也就是要指定的this值,如果没有传入,则默认为window全局对象; - 然后,谁将来调用
mycall,那么this就是谁,将其赋给context.fn; - 然后,通过参数伪数组将
context后面的参数取出来,并传给context.fn获得执行结果result; - 最后,删除掉
context.fn,并将result返回;
# 3. aplly 函数
# 3.1 描述
apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象 (opens new window))提供的参数。
# 3.2 语法
func.apply(thisArg, [argsArray]);
# 3.3 参数
- thisArg:可选的。在 func 函数运行时使用的
this值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式 (opens new window)下,则指定为null或undefined时会自动替换为指向全局对象,原始值会被包装。 - argsArray:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给
func函数。如果该参数的值为null或undefined,则表示不需要传入任何参数。从 ECMAScript 5 开始可以使用类数组对象。
# 3.4 返回值
调用有指定this值和参数的函数的结果。
# 3.5 实现
Function.prototype.myApply = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
if (context === null || context === undefined) {
context = window; // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
} else {
context = Object(context); // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
context.fn = this;
let result;
//判断是否存在第二个参数
//如果存在就将第二个参数也展开
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
实现思路:
- 首先,判断调用
myapply的是不是函数,如果不是,则直接抛出异常; - 接着,判断是否传入了第一个参数
context,也就是要指定的this值,如果没有传入,则默认为window全局对象; - 然后,谁将来调用
myapply,那么this就是谁,将其赋给context.fn; - 然后,判断是否传入了第二个参数,如果传入了则将其使用展开运算符
...传给context.fn获得执行结果result,如果没有传入,则直接调用context.fn获得执行结果result; - 最后,删除掉
context.fn,并将result返回;
# 4. bind 函数
# 4.1 描述
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。
# 4.2 语法
func.bind(thisArg[, arg1[, arg2[, ...]]])
# 4.3 参数
thisArg: 调用绑定函数时作为
this参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用bind在setTimeout中创建一个函数(作为回调提供)时,作为thisArg传递的任何原始值都将转换为object。如果bind函数的参数列表为空,执行作用域的this将被视为新函数的thisArg。arg1, arg2, ... : 当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。即与
call和apply的区别是,bind不是立即执行的,而是当调用的时候才执行;call和apply是立即执行的。
# 4.4 返回值
返回一个原函数的拷贝,并拥有指定的this值和初始参数。
# 4.5 实现
Function.prototype.mybind = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
const _this = this;
const args = [...arguments].slice(1);
//返回一个函数
return function F() {
if (this instanceof F) {
// this是否是F的实例 也就是返回的F是否通过new调用
return new _this(...args, ...arguments);
}
return _this.apply(context, args.concat(...arguments));
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
实现思路:
- 首先,判断调用
mybind的是不是函数,如果不是,则直接抛出异常; - 接着,谁将来调用
mybind,那么this就是谁,将其赋给_this,缓存一下; - 然后,通过参数伪数组将
context后面的参数(预先添加到绑定函数的参数)取出来,记作args, - 然后,返回一个函数,并判断如果使用
new运算符构造绑定函数,则忽略传入的第一个参数context,并将预先添加到绑定函数的参数args和将来传入新函数的参数arguments分别通过展开运算符...依次传入给调用mybind的调用者,并将结果返回。 - 最后,如果不是使用
new运算符构造绑定函数,则对调用者使用apply方法,将传入的第一个参数以及预先添加到绑定函数的参数args和将来传入新函数的参数arguments分别通过展开运算符...依次传入给调用mybind的调用者,并将结果返回;
# 5. softbind 函数
# 5.1 描述
bind函数多次调用会已第一次绑定的this为准,softbind已最后一次绑定传入的this为准;
# 5.2 实现
Function.prototype.softBind = function(obj) {
var fn = this;
var args = Array.prototype.slice.call(arguments, 1);
var bound = function() {
return fn.apply(
// 主要关注这一行: 判定当前this,如果绑定到了全局对象或undefined,null,
// 则修改this为传入的obj,否则什么也不做。
!this || this === (window || global) ? obj : this,
args.concat.apply(args, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
意义何在?
var name = "global";
const obj = {
name: "obj",
foo: function() {
console.log(this.name);
},
};
const obj1 = {
name: "obj1",
};
obj.foo(); // => obj // 常规方式
setTimeout(obj.foo, 0); // this丢失,global
// 现在我们使用软绑定
const softFoo = obj.foo.softBind(obj);
setTimeout(softFoo, 0); // obj
softFoo.call(obj1); // obj1,可以使用call显式改变this
obj1.foo = softFoo;
obj1.foo(); // obj1,也可以隐式改变this
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setTimeout(softFoo, 0)中softFoo同样会丢失this,但丢失后,通过内部判断我们将取上一步绑定的obj。
如果是硬绑定,则softFoo的this将被固定为obj,固定体现在始终应用第一次传入的这个上下文。
但是由于有了上面的判断,所以在应用隐式或显式规则时,由于this既不是全局对象也不是 undefined或null,所以判断为false,取后者this,而在我们调用softFoo.call(obj1)时,this又被改变了,变成了obj1,然后就修改成功了。
注意softFoo是调用softBind函数后返回的内部函数bound。还要注意softBind函数中第一行的 this,和bound中的 this,他们是不相等的。
可以看出,这个判断条件可以防止应用默认绑定规则,当应用了默认规则时,取前者。前者就是我们传入的目标上下文。否则便使用软绑定传入的上下文,就是我们的obj。
有了软绑定之后,排序为:new > 显式 > 隐式 > 软绑定 > 默认