它们有什么用及区别?
在阐述它们如何使用之前,我们有必要整理清楚this
的用法,简单的说this
是JavaScript
语言的一个关键字,它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。
那么问题又来了,this
的值是什么呢?
因为this
是在函数运行时,函数内部自动生成的一个对象,那么接下来我们通过函数来对this
进行分析。 首先JavaScript
中的函数可以分为两类:
- 常规函数:函数声明式,函数表达式,构造函数
- 箭头函数:(ES6引入使用)
接下来分别分析this
在这些函数中究竟是什么?
理解常规函数中的this
1.纯粹的函数调用
function test(name) {
console.log(name)
console.log(this)
}
test('Jerry') //调用函数
复制代码
以上函数调用的方式是非常常见的,然而这只是一种简写的形式,完整的写法应该如下:
function test(name) {
console.log(name)
console.log(this)
}
test.call(undefined, 'Tom')
复制代码
这里面便出现了我们将要学习的call
,先不讨论它的作用,我们继续讨论this
的用处,call
方法接受的第一个参数就是this
,但是我们这里是undefined
,按照规定,如果你传的context
是 null
或者 undefined
,那么 window
对象就是默认的context
(严格模式下默认context
是 undefined
)。
2.对象中函数的调用
const obj = {
name: 'Jerry',
greet: function() {
console.log(this.name)
}
}
obj.greet() //第一种调用方法
obj.greet.call(obj) //第二种调用方法
复制代码
从上面的例子中,我们发现这次call
方法的第一个参数为obj
,此时说明函数greet
内部的this
指向了obj
对象,这显而易见便知call
方法的作用是改变this
的指向,又因为上面两种调用方式结果一样可知函数的this
指向可以理解为谁调用便指向谁。
3.构造函数中的this
每个构造函数在new
之后都会返回一个对象,这个对象就是this
,也就是context
上下文。
理解箭头函数中的this
在使用箭头函数的时候,箭头函数会默认绑定外层的this
值,所以在箭头函数中this
的值和外层的this
是一样的。因为箭头函数没有this
,所以需要通过查找作用域链来确定this
的值。
这就意味着如果箭头函数被非箭头函数包含, this
绑定的就是最近一层非箭头函数的 this
。
注意:多层对象件套里面的this
是和最外层保持一致的。
因为今天的重点是讲解call
,apply
,bind
的用法及实现,然而箭头函数是没有这些方法的,所以箭头函数的使用仅限于此。
首先说明call
,apply
是ES5中的语法,bind
是ES6新引入的,它们三者的相似之处为:
- 都是用来改变函数的
this
对象的指向 - 第一个参数都是
this
要指向的对象 - 都可以利用后续参数进行传参
不同之处使用一个例子进行说明:
const personOne = {
name: "张三",
age: 12,
say: function () {
console.log(this.name + ',' + this.age);
}
}
const personTwo = {
name: "李四",
age: 24
}
personOne.say(); //张三,12
复制代码
对于以上的结果,我们应该都非常清楚,那么问题来了,如果我们想要知道personTwo
对象的信息如何实现呢?
分别使用call
,apply
以及bind
方法实现,并从中得到它们三者的区别:
personOne.say.call(personTwo); //李四,24
personOne.say.apply(personTwo); //李四,24
personOne.say.bind(personTwo); //没有输出任何东西
复制代码
修改以上代码对比可知:call
和apply
都是对函数的直接调用,而bind
方法返回的仍然是一个函数,因此我们需要执行它才会有结果。
personOne.say.call(personTwo); //李四,24
personOne.say.apply(personTwo); //李四,24
personOne.say.bind(personTwo)(); //李四,24
复制代码
接着继续讨论其余参数
const personOne = {
name: "张三",
age: 12,
say: function (gender, phone) {
console.log(this.name + ',' + this.age + ',' + gender + ',' + phone);
}
}
const personTwo = {
name: "李四",
age: 24
}
personOne.say("女", "123");
复制代码
这个例子的区别于上面的即为say
函数需要传递参数,我们分别使用这三种方法实现传递参数:
personOne.say.call(personTwo, "女", "123"); //李四,24,女,123
personOne.say.apply(personTwo, ["女", "123"]); //李四,24,女,123
personOne.say.bind(personTwo, "女", "123")(); //李四,24,女,123
复制代码
显而易见的区别call
和bind
除了第一个参数外,之后的参数均为一一传递,而apply
除了第一个参数外,只有一个参数即为一个数组,数组中的每一项为函数需要的参数。
说明它们的用法以及区别之后,我们就要自己尝试着剖析它的原理,自己书写这三个方法啦~~~~~
call
实现
在知道了它的使用即原理之后,想必直接看实现方法应该也可以理解的,那么先上代码:
Function.prototype.myCall = function (obj) {
const object = obj || window; //如果第一个参数为空则默认指向window对象
let args = [...arguments].slice(1); //存放参数的数组
object.func = this;
const result = object.func (...args);
delete object.func; //记住最后要删除掉临时添加的方法,否则obj就无缘无故多了个fn
return result;
}
复制代码
代码非常简短,一步步进行说明解释:
因为call
方法是每一个函数都拥有的,所以我们需要在Function.prototype
上定义myCall
,传递的参数obj
即为call
方法的第一个参数,说明this
的指向,如果没有该参数,则指向默认为window
对象。
args
为一个存放除第一个参数以外的其余参数的数组(arguments
为函数中接收到的多有参数,[...arguments]
可以将arguments
类数组转换为真正的数组,详细讲解可以查看ES6语法)。
解释object.func=this
之前,我们先使用示例使用一下自己定义的myCall
函数:
const personOne = {
name: "张三",
age: 12,
say: function (gender, phone) {
console.log(this.name + ',' + this.age + ',' + gender + ',' + phone);
}
}
const personTwo = {
name: "李四",
age: 24
}
Function.prototype.myCall = function (obj) {
const object = obj || window; //如果第一个参数为空则默认指向window对象
let args = [...arguments].slice(1); //存放参数的数组
object.func = this;
const result = object.func (...args);
delete object.func; //记住最后要删除掉临时添加的方法,否则object就无缘无故多了个func
return result;
}
personOne.say.myCall(personTwo,"女",18333669807); //李四,24,女,18333669807
复制代码
根据示例,我们进行解释,myCall
里面的this
指的是personOne.say
这个方法(因为myCall
是一个方法,上面所说的,谁调用它,它的this
便指向谁),object.func=this
相当于给object
这个对象克隆了一个personOne.say
方法,让object
在调用这个方法,相当于object.personOne.say
,达到了call
的效果。(记住最后要删除掉临时添加的方法,否则object
就无缘无故多了个func
)
object.func (...args)
里面的参数即为传入的其余参数。
apply
实现
通过上面的分析,想必大家应该已经基本明白了它是如何实现的了,那么接下来实现apply
就非常简单了,因为两者的区别主要就是参数的传递方式不同,和上面一样,先直接看一下代码:
Function.prototype.myApply = function (obj) {
const object = obj || window; //如果第一个参数为空则默认指向window对象
if (arguments.length > 1) {
var args = arguments[1]; //存放参数的数组
} else {
var args = []; //存放参数的数组
}
object.func = this;
const result = object.func(...args);
delete object.func; //记住最后要删除掉临时添加的方法,否则obj就无缘无故多了个fn
return result;
}
personOne.say.myApply(personTwo, ["女", 24]);
复制代码
主要区别就是获取参数不同,因为apply
的带二个参数为数组,数组中包含函数需要的各项参数值,其余内容实现myCall
相同,此处就不在做解释。
bind
实现
话不多说,依旧是先上代码
Function.prototype.myBind = function (obj) {
const object = obj || window; //如果第一个参数为空则默认指向window对象
let self = this;
let args = [...arguments].slice(1); //存放参数的数组
return function () {
let newArgs = [...arguments]
return self.apply(object, args.concat(newArgs))
}
}
personOne.say.myBind(personTwo, "女", 24)();
复制代码
前面的知识不重复说,return function
是因为bind
返回的是一个函数,并且这个函数不会执行,需要我们再次调用,那么当我们调用的时候,我们依旧可以对这个函数进行传递参数,即为支持柯里化形式传参,所以需要在返回的函数中声明一个空的数组接收调用bind
函数返回的函数时传递的参数,之后对两次的参数使用concat()
方法进行连接,调用ES5中的apply
方法。