前言
- 了解this的指向(文章中的调用方法与this息息相关)
- 了解过函数调用
- 了解过call()、apply()、bind()
目录:
- call、apply、bind 的区别分别是什么?
- call、apply、bind 的用法分别是什么?
- call、apply、bind 的用途在那些方面?
回顾
指针
“指向”、“指针”怎么理解?你可能会读到类似“这只是对象的指针,虽然删除了指向对象的指针,但对象依旧占用着内存”的语句。
打个比方:我们上学时(小学),老师会给学生们安排一个固定的座位号,目的是为了方便老师让学生回答问题时不用记住学生姓名,直接喊号,提高效率。那么,每个学生对应着一个座位号,如1,2,3分别代表小明,小红,小三,这里需要知道,座位号1,2,3和小明,小红,小三并不是完全相同的事物,前者是一个具有代表性的数字(宾语是数字),后者是实实在在的人(对象),但是两者又存在一一对应的关系,这时候,我们可以这样说,数字1指向小明,数字2指向小红,数字3指向小三(“指向”是动词),那么用图解的方式画出来就是1->小明,2->小红,3->小三。看看,数字与对象之间的箭头,就是指针(名词)。也就是说,“指针”就是数字与对象之间的关系的一种名词性的说法。
- “执行环境”怎么理解?你可能会读到类似“函数在被调用的时候,会被推入到它的执行环境中,函数的执行环境中存在哪些变量”的语句
打个比方:ECMAScript是一个导演,对象是演员(就那么几个如Object,Array,Math等),变量是道具。运行JavaScript代码相当于“导演让演员按照剧本借助一定的道具在舞台上演绎出一部话剧”,我们分析这句话,ECMAScript、对象、变量都有对应的喻体了,那剧本和舞台又是什么呢?剧本就是ECMAScript中制定的规则,舞台就是我们说的“执行环境”!一场话剧,在不同的阶段,需要上场的演员和需要使用的道具是不同的,所谓“你方唱罢我登场”,“执行环境”在不同阶段也是不同对象的表演舞台,存放的变量道具也不同。
■ call、apply、bind 的区别分别是什么?
三者异同
- 相同点:
三者都是用来改变函数的上下文,也就是this
指向的。
- 不同点:
fn.call
:立即调用,返回函数执行结果,this
指向第一个参数,后面可有多个参数,并且这些都是fn
函数的参数。】
fn.call(this参数,形参1,形参2...)
fn.apply
:立即调用,返回函数的执行结果,this
指向第一个参数,第二个参数是个数组,这个数组里内容是fn
函数的参数。
fn.apply(this参数,参数的数组)
fn.bind
: 不会立即调用,而是返回一个绑定后的新函数。
const newFn=fn.bind(this参数,函数参数1,函数参数2...)
应用场景
- 需要立即调用使用
call
/apply
- 要传递的参数不多,则可以使用
fn.call(thisObj, arg1, arg2 ...)
- 要传递的参数很多,则可以用数组将参数整理好调用
fn.apply(thisObj, [arg1, arg2 ...])
- 不需要立即执行,而是想生成一个新的函数长期绑定某个函数给某个对象使用,使用
const newFn = fn.bind(thisObj); newFn(arg1, arg2...)
实现原理
call
的实现
Function.prototype.myCall = function(thisObj = window){
thisObj.fn = this //此处this是指调用myCall的function
// 处理arguments
let arg = [...arguments].slice(1)
let result = thisObj.fn(...arg) // 执行该函数
delete thisObj.fn
return result
}
验证一下
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.myCall(foo, 'kevin', 18);
// kevin
// 18
// 1
apply
的实现,和call类似
Function.prototype.myApply = function (context = window, arr){
context.fn = this
let result
if(!arr){
result = context.fn()
} else {
result = context.fn(...arr)
}
delete context.fn
return result
}
bind
的实现
bind
的实现要借助刚才的apply
Function.prototype.myBind = function(context){
// 类型判断
if(typeof this !== 'function'){
throw new TypeError('must be a function')
}
let self = this // 这个this是调用bind的函数
let argsArr = [...arguments].slice(1)
return function(){
let bindFuncArg = [...arguments]
// 合并两次传的参数
let totalArgs = argsArr.concat(bindFuncArg)
return self.apply(context, totalArgs)
}
}
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中的定义:
现在我们在回想一下前面的函数调用,那就很好理解了,我们在声明函数时,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("世界"); // 打印出// 掘金果酱淋 你好啊 世界
怎么样,挺简单的吧!
使用 Function.prototype.bind
因为有时引用具有持久性this
值的函数可能会很方便,所以人们一直使用简单的闭包技巧将函数转换为不变的函数this
:
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this.name + " says hello " + thing);
}
}
var boundHello = function(thing) { return person.hello.call(person, thing); }
boundHello("world");
即使我们的boundHello
呼叫仍然不满意boundHello.call(window, "world")
,我们也会转过来使用原始call
方法将this
值更改回我们想要的值。
我们可以通过一些调整使此技巧通用:
var bind = function(func, thisValue) {
return function() {
return func.apply(thisValue, arguments);
}
}
var boundHello = bind(person.hello, person);
boundHello("world") // "Brendan Eich says hello world"
为了理解这一点,您只需要另外两个信息。首先,arguments
是一个类似数组的对象,它表示传递给函数的所有参数。其次,该apply
方法的工作方式与call
原始方法完全相同,不同之处在于它采用了一个类似于Array的对象,而不是一次列出一个参数。
我们的bind
方法只是返回一个新函数。调用它时,我们的新函数将简单地调用传入的原始函数,将原始值设置为this
。它还通过参数传递。
因为这是一个有点普遍的习惯用法,所以ES5 bind
在所有Function
实现此行为的对象上引入了一种新方法:
var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"
当您需要一个原始函数作为回调传递时,这是最有用的:
var person = {
name: "Alex Russell",
hello: function() { console.log(this.name + " says hello world"); }
}
$("#some-div").click(person.hello.bind(person));
// when the div is clicked, "Alex Russell says hello world" is printed
当然,这有些笨拙,并且TC39(负责ECMAScript下一版本的委员会)继续致力于开发一种更加优雅,仍然向后兼容的解决方案。
JavaScript中call,apply,bind方法的总结。 - 追梦子 - 博客园www.cnblogs.com Function.prototype.bind()developer.mozilla.org