整理call、apply、bind这三个方法的的知识点。
之前这篇文章提到过this的各种情况,其中有一种情况就是通过call、apply、bind来将this绑定到指定的对象上。
也就是说,这三个方法可以改变函数体内部this的指向。
这三个方法有什么区别呢?分别适合应用在哪些场景中呢?
先举个简单的栗子 ~
var person = {
name: "axuebin",
age: 25
};
function say(job){
console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"FE"); // axuebin:25 FE
say.apply(person,["FE"]); // axuebin:25 FE
var sayPerson = say.bind(person,"FE");
sayPerson(); // axuebin:25 FE
对于对象person而言,并没有say这样一个方法,通过call/apply/bind就可以将外部的say方法用于这个对象中,其实就是将say内部的this指向person这个对象。
call
call是属于所有Function的方法,也就是Function.prototype.call。
The call() method calls a function with a given this value and arguments provided individually.
call() 方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。
它的语法是这样的:
fun.call(thisArg[,arg1[,arg2,…]]);
其中,thisArg就是this指向,arg是指定的参数。
call的用处简而言之就是可以让call()中的对象调用当前对象所拥有的function。
ECMAScript规范
ECMAScript规范中是这样定义call的:
当以thisArg和可选的arg1,arg2等等作为参数在一个func对象上调用call方法,采用如下步骤:
- 如果
IsCallable(func)是false, 则抛出一个TypeError异常。 - 令
argList为一个空列表。 - 如果调用这个方法的参数多余一个,则从
arg1开始以从左到右的顺序将每个参数插入为argList的最后一个元素。 - 提供
thisArg作为this值并以argList作为参数列表,调用func的[[Call]]内部方法,返回结果。
call方法的length属性是1。
在外面传入的thisArg值会修改并成为this值。thisArg是undefined或null时它会被替换成全局对象,所有其他值会被应用ToObject并将结果作为this值,这是第三版引入的更改。
使用call调用函数并且指定this
var obj = {
a: 1
}
function foo(b, c){
this.b = b;
this.c = c;
console.log(this.a + this.b + this.c);
}
foo.call(obj,2,3); // 6
call实现继承
在需要实现继承的子类构造函数中,可以通过call调用父类构造函数实现继承。
function Person(name, age){
this.name = name;
this.age = age;
this.say = function(){
console.log(this.name + ":" + this.age);
}
}
function Student(name, age, job){
Person.call(this, name ,age);
this.job = job;
this.say = function(){
console.log(this.name + ":" + this.age + " " + this.job);
}
}
var me = new Student("axuebin",25,"FE");
console.log(me.say()); // axuebin:25 FE
apply
apply也是属于所有Function的方法,也就是Function.prototype.apply。
The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object).
apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数。
它的语法是这样的:
fun.apply(thisArg, [argsArray]);
其中,thisArg就是this指向,argsArray是指定的参数数组。
通过语法就可以看出call和apply的在参数上的一个区别:
call的参数是一个列表,将每个参数一个个列出来apply的参数是一个数组,将每个参数放到一个数组中
ECMAScript规范
当以thisArg和argArray为参数在一个func对象上调用apply方法,采用如下步骤:
- 如果
IsCallable(func)是false, 则抛出一个TypeError异常 . - 如果
argArray是null或undefined, 则 - 返回提供
thisArg作为this值并以空参数列表调用func的[[Call]]内部方法的结果。 - 如果
Type(argArray)不是Object, 则抛出一个TypeError异常 . - 令
len为以"length"作为参数调用argArray的[[Get]]内部方法的结果。 - 令
n为ToUint32(len). - 令
argList为一个空列表 . - 令
index为0. - 只要
index<n就重复 - 令
indexName为ToString(index). - 令
nextArg为以indexName作为参数调用argArray的[[Get]]内部方法的结果。 - 将
nextArg作为最后一个元素插入到argList里。 - 设定
index为index + 1. - 提供
thisArg作为this值并以argList作为参数列表,调用func的[[Call]]内部方法,返回结果。
apply方法的length属性是 2。
在外面传入的thisArg值会修改并成为this值。thisArg是undefined或null时它会被替换成全局对象,所有其他值会被应用ToObject并将结果作为this值,这是第三版引入的更改。
用法
在用法上apply和call一样,就不说了。
实现一个apply
参考链接:https://github.com/jawil/blog/issues/16
第一步,绑定上下文
Function.prototype.myApply=function(context){
// 获取调用`myApply`的函数本身,用this获取
context.fn = this;
// 执行这个函数
context.fn();
// 从上下文中删除函数引用
delete context.fn;
}
var obj ={
name: "xb",
getName: function(){
console.log(this.name);
}
}
var me = {
name: "axuebin"
}
obj.getName(); // xb
obj.getName.myApply(me); // axuebin
确实成功地将this指向了me对象,而不是本身的obj对象。
第二步,给定参数
上文已经提到apply需要接受一个参数数组,可以是一个类数组对象,还记得获取函数参数可以用arguments吗?
Function.prototype.myApply=function(context){
// 获取调用`myApply`的函数本身,用this获取
context.fn = this;
// 通过arguments获取参数
var args = arguments[1];
// 执行这个函数,用ES6的...运算符将arg展开
context.fn(...args);
// 从上下文中删除函数引用
delete context.fn;
}
var obj ={
name: "xb",
getName: function(age){
console.log(this.name + ":" + age);
}
}
var me = {
name: "axuebin"
}
obj.getName(); // xb:undefined
obj.getName.myApply(me,[25]); // axuebin:25
context.fn(...arg)是用了ES6的方法来将参数展开,如果看过上面那个链接,就知道这里不通过...运算符也是可以的。
原博主通过拼接字符串,然后用eval执行的方式将参数传进context.fn中:
for (var i = 0; i < args.length; i++) {
fnStr += i == args.length - 1 ? args[i] : args[i] + ',';
}
fnStr += ')';//得到"context.fn(arg1,arg2,arg3...)"这个字符串在,最后用eval执行
eval(fnStr); //还是eval强大
第三步,当传入apply的this为null或者为空时
我们知道,当apply的第一个参数,也就是this的指向为null时,this会指向window。知道了这个,就简单了~
Function.prototype.myApply=function(context){
// 获取调用`myApply`的函数本身,用this获取,如果context不存在,则为window
var context = context || window;
context.fn = this;
//获取传入的数组参数
var args = arguments[1];
if (args == undefined) { //没有传入参数直接执行
// 执行这个函数
context.fn()
} else {
// 执行这个函数
context.fn(...args);
}
// 从上下文中删除函数引用
delete context.fn;
}
var obj ={
name: "xb",
getName: function(age){
console.log(this.name + ":" + age);
}
}
var name = "window.name";
var me = {
name: "axuebin"
}
obj.getName(); // xb:25
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [25]); // window.name:25
obj.getName.myApply(me, [25]); // axuebin:25
第四步 保证fn函数的唯一性
ES6中新增了一种基础数据类型Symbol。
const name = Symbol();
const age = Symbol();
console.log(name === age); // false
const obj = {
[name]: "axuebin",
[age]: 25
}
console.log(obj); // {Symbol(): "axuebin", Symbol(): 25}
console.log(obj[name]); // axuebin
所以我们可以通过Symbol来创建一个属性名。
var fn = Symbol();
context[fn] = this;
完整的apply
Function.prototype.myApply=function(context){
// 获取调用`myApply`的函数本身,用this获取,如果context不存在,则为window
var context = context || window;
var fn = Symbol();
context[fn] = this;
//获取传入的数组参数
var args = arguments[1];
if (args == undefined) { //没有传入参数直接执行
// 执行这个函数
context[fn]()
} else {
// 执行这个函数
context[fn](...args);
}
// 从上下文中删除函数引用
delete context.fn;
}
这样就是一个完整的apply了,我们来测试一下:
var obj ={
name: "xb",
getName: function(age){
console.log(this.name + ":" + age);
}
}
var name = "window.name";
var me = {
name: "axuebin"
}
obj.getName(); // xb:25
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [25]); // window.name:25
obj.getName.myApply(me, [25]); // axuebin:25
ok 没啥毛病 ~
再次感谢1024大佬 ~
bind
The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
语法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
其中,thisArg就是this指向,arg是指定的参数。
可以看出,bind会创建一个新函数(称之为绑定函数),原函数的一个拷贝,也就是说不会像call和apply那样立即执行。
当这个绑定函数被调用时,它的this值传递给bind的一个参数,执行的参数是传入bind的其它参数和执行绑定函数时传入的参数。
用法
当我们执行下面的代码时,我们希望可以正确地输出name,然后现实是残酷的
function Person(name){
this.name = name;
this.say = function(){
setTimeout(function(){
console.log("hello " + this.name);
},1000)
}
}
var person = new Person("axuebin");
person.say(); //hello undefined
这里this运行时是指向window的,所以this.name是undefined,为什么会这样呢?看看MDN的解释:
由setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致,这些代码中包含的 this 关键字在非严格模式会指向 window。
有一个常见的方法可以使得正确的输出:
function Person(name){
this.name = name;
this.say = function(){
var self = this;
setTimeout(function(){
console.log("hello " + self.name);
},1000)
}
}
var person = new Person("axuebin");
person.say(); //hello axuebin
没错,这里我们就可以用到bind了:
function Person(name){
this.name = name;
this.say = function(){
setTimeout(function(){
console.log("hello " + this.name);
}.bind(this),1000)
}
}
var person = new Person("axuebin");
person.say(); //hello axuebin
MDN的Polyfill
Function.prototype.bind = function (oThis) {
var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fNOP = function () {};
var fBound = function () {
fBound.prototype = this instanceof fNOP ? new fNOP() : fBound.prototype;
return fToBind.apply(this instanceof fNOP ? this : oThis || this, aArgs )
}
if( this.prototype ) {
fNOP.prototype = this.prototype;
}
return fBound;
}
总结
- 三者都是用来改变函数的
this指向 - 三者的第一个参数都是
this指向的对象 bind是返回一个绑定函数可稍后执行,call、apply是立即调用- 三者都可以给定参数传递
call给定参数需要将参数全部列出,apply给定参数数组