前言:
我希望在开始读这篇文章之前,你了解过函数调用、this
指向、call()
、apply()
、bind()
,当然,只要了解过就好,因为本文就是为了让你更好的理解它们。(转载请注明出处---掘金果酱淋)
开篇:
我将说明几个你在阅读下文时可能会觉得困惑的概念,当你觉得疑惑时,可以回到这里来看看。
- “指向”、“指针”怎么理解?你可能会读到类似“这只是对象的指针,虽然删除了指向对象的指针,但对象依旧占用着内存”的语句。
打个比方:我们上学时(小学),老师会给学生们安排一个固定的座位号,目的是为了方便老师让学生回答问题时不用记住学生姓名,直接喊号,提高效率。那么,每个学生对应着一个座位号,如1,2,3分别代表小明,小红,小三,这里需要知道,座位号1,2,3和小明,小红,小三并不是完全相同的事物,前者是一个具有代表性的数字(宾语是数字),后者是实实在在的人(对象),但是两者又存在一一对应的关系,这时候,我们可以这样说,数字1指向小明,数字2指向小红,数字3指向小三(“指向”是动词),那么用图解的方式画出来就是1->小明,2->小红,3->小三。看看,数字与对象之间的箭头,就是指针(名词)。也就是说,“指针”就是数字与对象之间的关系的一种名词性的说法。
- “执行环境”怎么理解?你可能会读到类似“函数在被调用的时候,会被推入到它的执行环境中,函数的执行环境中存在哪些变量”的语句
打个比方:ECMAScript是一个导演,对象是演员(就那么几个如Object
,Array
,Math
等),变量是道具。运行JavaScript代码相当于“导演让演员按照剧本借助一定的道具在舞台上演绎出一部话剧”,我们分析这句话,ECMAScript、对象、变量都有对应的喻体了,那剧本和舞台又是什么呢?剧本就是ECMAScript中制定的规则,舞台就是我们说的“执行环境”!一场话剧,在不同的阶段,需要上场的演员和需要使用的道具是不同的,所谓“你方唱罢我登场”,“执行环境”在不同阶段也是不同对象的表演舞台,存放的变量道具也不同。
call()
,apply()
方法几乎一样,只是传入参数的方式不一样,后者第二个参数是一个数组,那为什么要存在着两个几乎一样的东西?这不是重复造轮子么?
这需要介绍它们使用场景来告诉你原因:
Math
对象有个max()
方法: 可以返回传入数字中的最大者:
var max = Math.max(1, 8, 3, 15, 4, 5); // 调用Math的max()取得传入的最大参数并赋值给max
console.log(max); // 打印出15
上面的例子可以写成这样,效果与上面的完全一样:
var max = Math.max.call(Math, 1, 8, 3, 15, 4, 5); // 调用Math的max()取得传入的最大参数并赋值给max(函数调用的小动作)
console.log(max); // 打印出15
那么接下来我换个需求,我想要让你结合max()
方法,找出一个数字数组中最大的数字。你可能会说,简单啊,然后给出了这些方案!
var arr = [1, 8, 3, 15, 4, 5]; //声明数组表达式
var max = Math.max.call(Math, ...arr); // ES6解构语法======(这里有疑问看正文后再回来消化下)
console.log(max); // 打印15
可以的,很机智地实现了需求。但是你回想一下apply()
的第二个参数是什么?数组!看代码
var arr = [1, 8, 3, 15, 4, 5]; //声明数组表达式
var max = Math.max.apply(Math, arr); // apply方法
console.log(max); // 打印15
有没有那种 “我正好需要,你正好专业” 的感觉~!
正文:function
调用的“小秘密”
不要被开篇的东西吓到,本文的正文很简单的。就是告诉你function
调用时你不知道的“小动作”(跟this
相关的)。
先要知道: 函数中,this
和arguments
这两个函数的属性,只要在函数执行的时候才会知道它们分别是指向谁(好好琢磨一下这话)。
1. 一般函数的调用(全局环境中函数的调用)
首先,一般地,我们在全局环境中声明函数和执行函数的过程如下:
function hello(someone) {
console.log(this + "你好啊 " + someone);
} // 函数声明
hello("掘金果酱淋"); // 函数调用,打印出 //[object Window]你好啊 掘金果酱淋
其实,函数在内部执行的时候,还做了个小动作,也就是我们要说的小秘密:
function hello(someone) {
console.log(this + "你好啊 " + someone);
} // 函数声明
hello.call(window, "掘金果酱淋"); // 函数调用,打印出 //[object Window]你好啊 掘金果酱淋
对比一下,发现重点了么?函数在执行的时候,自动将this
指向了window
这个全局对象(注意node环境下全局对象是global
),与我们手动让this
指向window
打印出来的结果一样!
那你可能还会反驳,书上不是说,在严格模式下(“use strict”
)时,this
时指向了undefinded
,那是因为在严格模式下,函数调用的小动作是这样的:
function hello(someone) {
'use strict';
console.log(this + "你好啊 " + someone);
} // 函数声明
hello("掘金果酱淋"); // 函数调用,打印出 //undefined你好啊 掘金果酱淋
hello.call(undefined, "掘金果酱淋"); // 打印出 //undefined你好啊 掘金果酱淋
怎么样,有没有豁然开朗的感觉?
2. 对象方法的调用(对象里面的函数的调用)
首先,存在这样一个对象,平常的调用这样的:
var person = {
name: "掘金果酱淋",
hello: function(someone) {
console.log(this + " 你好啊 " + someone);
}
};
// 正常调用
person.hello("world");// [object Object] 你好啊 世界
有了之前的解密,相信你能理解函数在调用时的小动作是这样的:
var person = {
name: "掘金果酱淋",
hello: function(someone) {
console.log(this + " 你好啊 " + someone);
}
};
// 小动作
person.hello.call(person, "world");// [object Object] 你好啊 世界
call()
, apply()
, bind()
的原理很简单啊!
经过前面解析函数调用的小秘密,我们知道它们都“偷偷地”调用了call()
!
我们再看一下它们在MDN中的定义:
fun.call(thisArg, arg1, arg2, ...)
参数
thisArg 在 fun 函数运行时指定的 this 值。if(thisArg == undefined|null) this = window,if(thisArg == number|boolean|string) this == new Number()|new Boolean()| new String()
arg1, arg2, ... 指定的参数列表。
func.apply(thisArg, [argsArray])
参数
thisArg 可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装
argsArray 可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。 浏览器兼容性 请参阅本文底部内容。。
function.bind(thisArg[, arg1[, arg2[, ...]]])
参数
thisArg 调用绑定函数时作为this参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用bind在setTimeout中创建一个函数(作为回调提供)时,作为thisArg传递的任何原始值都将转换为object。如果bind函数的参数列表为空,执行作用域的this将被视为新函数的thisArg。
arg1, arg2, ... 当目标函数被调用时,预先添加到绑定函数的参数列表中的参数
现在我们在回想一下前面的函数调用,那就很好理解了,我们在声明函数时,this
和arguments
无法知道是谁(前面说过),那就是undefinded
或者null
,所以根据MDN的定义,在调用函数是,函数的小动作偷偷调用call()
方法,为函数设置了this
对象。apply()
同理!只不过你要注意它接收参数的方式不同。
那bind()
呢,也很好理解了,我们不想在函数执行时才被函数的小动作指定this
对象,而是要固定this
对象,那么bind()
方法就是在内部调用了call()
或者apply()
方法主动指定this
对象,同时为了函数可以复用,借用了闭包来保存这个this
对象(闭包这里不多说),以下是模拟bind()
方法的示例:
// 定义一个对象
var person = {
name: "掘金果酱淋",
hello: function(thing) {
console.log(this.name + " 你好啊 " + thing);
}
};
// 模拟bind方法的操作,接收一个函数和一个this对象(执行环境)
var bind = function(func, thisValue) {
return function() {
return func.apply(thisValue, arguments); // 注意apply()和arguments的妙用
};
};
var boundHello = bind(person.hello, person);
boundHello("世界"); // 打印出// 掘金果酱淋 你好啊 世界
怎么样,挺简单的吧!
后话:this
对象到底是什么?
this
对象就是函数的执行环境(觉得不理解看一下开篇部分),我说过,执行环境是舞台,函数就是演员,函数可以调用的变量是表演需要的道具。那么,改变函数的执行环境有什么意义呢?我们看例子:
var nullArr = []; // 空数组
var arrType = Object.prototype.toString.call(nullArr); // 调用Object对象原型中的方法,同时将执行环境(this)指向 nullArr
console.log(arrType); // 打印 // [object Array]
console.log(nullArr.toString()); // 空字符串
首先,我们需要知道,
-
Object.prototype
是所有对象的终端原型对象,其中包括的属性方法是所有对象共享的,其他对象也可以 重写 那些在终端原型对象中的方法 -
几乎所有的引用对象都有自己的
toString()
且是重写了的;
上面例子中,Object.prototype.toString()
是终端原型对象的方法,而nullArr
作为一个数组的实例,只能调用自己Array.prototype
原型中的toString()
;从例子中我们可以知道,一个空数组调用自己Array.prototype
原型的toString()
只能得到一个空字符串
在特殊情况下,那我们想要数组实例nullArr
能够使用Object.prototype.toString()
方法,简单的方法就是给数组重写一个这样的方法,但是如果每一个需要该方法的数组都重新写一次,这就很不符合复用的原则了。
那我们调用call()
方法将Object.prototype.toString()
方法的执行环境(this
)主动变成了nullArr
,那么这个方法就可以调用这个执行环境中的变量了(舞台道具),从nullArr
的角度看,等于它拥有了Object.prototype.toString()
方法,其实应该说拥有了Object.prototype.toString()
方法的指针(看开篇),注意指针只是一种关系,而不是重写了对象,(当然,这种关系只在call()
执行时有,执行结束后就没有了—退下舞台)。
结语:很简单是假的
能够看完和理解上面的分析过程,你可能会得出了一个结论:这不简单啊,挺绕的!不得不承认,我说谎了,其实并不简单。然而我是为了让你有信心看下去,事情做过之后就会简单了。
我觉得人对未知的东西都会有一定的恐惧感,但如果有人一直强调很简单,那么便不会连开始尝试的勇气都没有!
参考文章:
- 追梦者-JavaScript中call,apply,bind方法的总结
- 追梦者-彻底理解js中this的指向,不必硬背
- Yehuda Katz-Understanding JavaScript Function Invocation and "this"(理解JavaScript函数调用和“this”)(这篇才是顿悟的来源)
QQ:1448373124(欢迎交流前端技术,对于文章疏漏处欢迎指正)