call 和 apply
call
和 apply
是 Function
构造函数原型对象上的方法,所有的函数都可以调用 call
和 apply
,作用是可以改变调用 call
和 apply
函数内部的 this
指向,并执行函数。
call 的使用方法
/* 不指定 this */
function fn() {
console.log(this, arguments);
}
fn.call();
// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
// Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
不指定替换的 this
,则调用 call
的函数在运行时决定 this
指向,当前案例中在浏览器中运行,则指向 window
对象。
/* 一次调用 call */
function fn() {
console.log(this, arguments);
}
fn.call('hello', '1', '2');
// String { "hello" }
// Arguments(2) ["1", "2", callee: ƒ, Symbol(Symbol.iterator): ƒ]
在上面案例中,call
将 fn
内部的 this
更改为 hello
的基本包装类(对象),而 1
和 2
作为 fn
的参数,以 arguments
的形式被打印出来。
/* 多次调用 call */
function fn1() {
console.log(this, arguments);
}
function fn2() {
console.log(this, arguments);
}
fn1.call.call(fn2, '1', '2');
// String { "1" }
// Arguments ["2", callee: ƒ, Symbol(Symbol.iterator): ƒ]
由于 call
是函数原型的方法,当然也可以被 call
自己调用,在上面的案例中,第一个 call
内部的 this
为调用者 fn1
,通过第二个 call
将第一个 call
内部的 this
由 fn1
更改为 fn2
,1
和 2
作为参数传递给第一个 call
,而 1
又作为第一个 call
内部 this
指向的 fn2
内部的 this
,2
作为 fn2
的参数,最后执行 fn2
,固有上面执行结果。
call 的实现原理
根据 call
方法的特点,来模拟实现一个自己封装的 call
方法,代码如下。
/* call 的实现原理 */
// context 参数为要替换的 this
Function.prototype.call = function (context) {
// 将传入的 this 转换成对象,若没传则使用 window 作为 this
context = context ? Object(context) : window;
// 将调用 call 的函数作为属性赋值给传入的 this
context.fn = this;
var args = [];
// 将传递给调用 call 函数的参数转化成字符串取值的形式
for (var i = 1; i < arguments.length; i++) {
// args ['arguments[1]', 'arguments[2]']
args.push('arguments[' + i + ']');
}
// 利用 eval 执行 context.fn,并利用数组转换字符串的 toString 去掉 [ ]
var result = eval('context.fn(' + args + ')');
// 删除 context 上多余的 fn 属性
delete context.fn;
return result;
}
上面的实现方式重点解决两个问题:
- 如何让调用
call
函数内部的this
指向传入的this
,我们通过将传入this
上加一个属性fn
,值为调用call
的函数,在执行函数时并不直接调用this
,而是执行context.fn
,这样内部的this
指向了调用者context
,即指向了传入要替换的this
;- 如何将
call
调用时除第一个参数以外的参数列表作为调用call
函数的参数依次传入,我们这里借用了eval
提供执行环境,将要执行的代码拼接成字符串,这样就可以容易的将argument
第二项后面的所有项通过循环的方式拼接。
给
context
添加的多余属性fn
,要在函数context.fn
执行后删除。
apply 的使用方法
apply
与 call
的作用相同,基本用法如下。
/* apply 用法 */
function fn() {
console.log(this, arguments);
}
fn.apply('hello', ['1', '2']);
// String { "hello" }
// Arguments(2) ["1", "2", callee: ƒ, Symbol(Symbol.iterator): ƒ]
apply 的实现原理
通过用法可以看出 apply
与 call
唯一不同的就是传参方式,call
传递给调用它的函数传参靠调用时使用参数列表的方式依次传入,而 apply
是通过数组的方式传入,只需要将 call
的代码稍加改造就可以实现 apply
。
/* apply 的实现原理 */
// context 参数为要替换的 this,args 为调用 apply 函数执行的参数
Function.prototype.apply = function (context, args) {
context = context ? Object(context) : window;
context.fn = this;
var result;
// 判断是否传入参数列表,如果没传则直接执行
if (!args) {
result = context.fn();
} else {
result = eval('context.fn(' + args + ')');
}
delete context.fn;
return result;
}
bind
bind 的使用方法
bind
函数是 Function
原型对象上的方法,bind
的作用是可以将调用它的函数内部的 this
绑定成所指定的 this
,第一个参数为指定的 this
,与 call
和 apply
不同的是,调用 bind
的函数并不会执行,而是返回一个新的函数,新的函数调用时传入的参数会和 bind
调用时传入的除第一个以外的参数进行合并,并作为调用 bind
的函数执行的参数,下面是 bind
的基本用法。
/* 返回的函数当做普通函数调用 */
var obj = {
name: 'Shen'
};
function sayName() {
console.log(this.name);
}
var bindFn = sayName.bind(obj);
bindFn();
// Shen
/* 调用 bind 和执行返回函数分开传参 */
var obj = {
name: 'Shen'
};
function animal(name, age) {
console.log(this.name + ' have a ' + name + ' is ' + age + ' years old.');
}
var bindFn = animal.bind(null, 'cat');
bindFn(2);
// Shen have a cat is 2 years old.
/* 返回的函数当做构造函数执行 */
var obj = {
name: 'Shen'
};
function Animal(name, age) {
this.name = name;
this.age = age;
}
// 动物类别为哺乳类
Animal.prototype.category = 'mammalia';
var BindFn = Animal.bind(obj, 'cat');
var cat = new BindFn(2);
console.log(cat); // Animal {name: "cat", age: 2}
console.log(cat.category); // mammalia
bind 的原理
从上面的例子已经可以看出 bind
不但能绑定 this
,收集参数,返回的函数既可以直接调用,又可以作为构造函数实例化对象,而实例化的对象的方式,bind
绑定的 this
不生效,this
指向被创建的实例,实例依然可以找到原来函数原型上的属性和方法,根据 bind
的特性,模拟实现的代码如下。
/* bind 的实现原理 */
// context 参数为要绑定的 this
Function.prototype.bind = function (context) {
// this 为调用 bind 的函数
var self = this;
// 收集除了 context 以外所有的参数
var bindArgs = Array.prototype.slice.call(arguments, 1);
// 返回的新函数 fBound
function fBound() {
// 收集 fBound 的参数
var args = Array.prototype.slice.call(arguments);
// 执行调用 bind 的函数
// 若是普通函数调用,this 为 context,若是作为构造函数则 this 为实例
self.apply(this instanceof fBound ? this : context, bindArgs.concat(args));
}
// 用来继承的中间函数
function fNOP() {}
// 作为构造函数调用 fBound 时,为了实例可以找到调用 bind 函数的原型对象,进行继承
if (this.prototype) {
// Function.prototype 为函数,可以调用 bind,当时没有原型对象,所以要判断
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
}
return fBound;
}
在上面代码中,如果调用
bind
返回函数作为构造函数使用,则需要通过继承找回原函数的属性和方法,但是有一个特例,就是Function.prototype
,类型为函数,却没有prototype
属性,所以需要判断。
最后
上面就是
call
、apply
和bind
的基本用法、实现原理以及区别,希望读者可以通过这篇文章加深对call
、apply
和bind
的印象,运用自如。