# 实现call/apply/bind
考点:
- call/apply/bind的功能
- js中this的指向
- 原型链
# js中this的指向
1.当函数作为构造函数, 通过 new xxx() 调用时, this指向生成的实例
function cat(name, color) {
this.name = name
this.color = color
}
let cat1 = new cat('大毛','橘色') //this指向cat1
let cat2 = new cat('二毛','黑色') //this指向cat2
console.log(cat1)
console.log(cat2)
2
3
4
5
6
7
8
2.当函数直接被调用时 (通过 xxx()的方式调用) this指向window对象, 严格模式下为 undefined
function Dog(name, color) {
this.name = name
this.color = color
}
Dog('大毛','橘色')
console.log(window.name) // 大毛
console.log(window.color) // 橘色
2
3
4
5
6
7
3.当函数被调用时, 它是作为某个对象的方法 (前面加了点 '.') this指向这个对象 (点 '.' 前面的对象) (谁调用它, this就指向谁)
function setDetails(name, color) {
this.name = name
this.color = color
}
let pig = {}
pig.setDetails = setDetails
pig.setDetails('大毛','红色')
console.log(pig.name) // 大毛
console.log(pig.color) // 红色
2
3
4
5
6
7
8
9
思考1
let obj = {
x:10,
fn: function() {
function a() {
console.log(this.x)
console.log(this) // this 指向 window
}
a()
}
}
obj.fn() // 输出什么? undefined
2
3
4
5
6
7
8
9
10
11
思考2
let obj2 = {
x: 10,
fn:function() {
console.log(this.x)
}
}
let a = obj2.fn
obj2.fn() // 输出什么? 10
a() // 输出什么? undefined
2
3
4
5
6
7
8
9
思考3
let obj3 = {
x:10,
fn: function() {
return function() {
console.log(this.x)
}
}
}
obj3.fn()() // 输出什么 undefined
2
3
4
5
6
7
8
9
# 实现call
函数通过call
调用时, 函数体内的this
指向call
方法传入的第一个实参, 而call
方法后续的实参会依次传入作为原函数的实参传入
function setDetails(name, color) {
this.name = name
this.color = color
}
let cat1 = {}
let cat2 = {}
setDetails.call(cat1, '大毛','橘色')
setDetails.call(cat2, '二毛','黑色')
console.log(cat1.name) // 大毛
console.log(cat2.name) // 二毛
2
3
4
5
6
7
8
9
10
let person1 = {
name: 'zs',
say: function(hobby) {
console.log(this.name)
console.log('爱好:' + hobby)
}
}
let person2 = {
name: 'ls'
}
person1.say('打游戏')
person1.say.call(person2,'健身')
2
3
4
5
6
7
8
9
10
11
12
开始实现:
先在原型链上挂上我们自定义的 call2
方法, 让所有函数共享此方法
Function.prototype.call2 = function() {
console.log(this)
}
setDetails.call2()
2
3
4
在 call2
中通过 this
拿到调用 call2
的原函数, 接下来通过上面提到 当前函数被调用时, 它是作为某个对象的方法 (前面加了点'.') this指向这个对象 (点 '.'前面的对象) 改变原函数中 this
的指向
function setDetails() {
this.name = '大毛'
}
Function.prototype.call2 = function(context) {
// this === 原函数
console.log(this)
// 将原函数作为cat1的方法调用
context.setDetails = this
context.setDetails()
}
let cat1 = {}
setDetails.call2(cat1)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
继续改进
- 其实将原函数作为
context
的方法调用时, 方法名并不影响功能, 将方法名写死反而会造成方法名冲突. 在ES6可以用Symbol
来解决, 如果不用Symbol
可以随机生成一个基本不可能冲突的字符串, 万一冲突则继续生成到不冲突为止 - 在方法调用后删除方法, 避免给
context
增加多余的方法 - 原函数可能有返回值, 要将改变
this
并调用后的返回值也返回
<script>
function setDetails() {
this.name = '大毛'
return 123
}
Function.prototype.call2 = function(context) {
function mySymbol(obj) {
let unique = (Math.random + new Date())
if(obj.hasOwnProperty(unique)) {
return mySymbol(obj)
}else {
return unique
}
}
let uniqueName = mySymbol(context)
context[uniqueName] = this
let result = context[uniqueName]()
// 删除临时方法
delete context[uniqueName]
return result
}
let cat1 = {}
let result = setDetails.call2(cat1)
console.log(result)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
改进解决参数传递的问题:
- 因为不知道用户在调用时传参的个数, 解决可以通过
arguments
或者剩余参数
来获取除了context
剩余的参数, 将剩余参数传递给context[uniqueName]
<script>
function setDetails(name,color) {
this.name = name
this.color = color
}
Function.prototype.call2 = function(context) {
function mySymbol(obj) {
let unique = (Math.random + new Date())
if(obj.hasOwnProperty(unique)) {
return mySymbol(obj)
}else {
return unique
}
}
let uniqueName = mySymbol(context)
// 获取除了第一个参数外剩余的参数
let args = Array.from(arguments).slice(1)
// this === 原函数
// 将原函数作为 cat1的方法调用
context[uniqueName] = this
// 使用扩展运算符传参, 可以解决参数不确定的问题
let result = context[uniqueName](...args) // 等同于 context[uniqueName]('大毛', '橘色')
// 删除临时方法
delete context[uniqueName]
return result
}
let cat1 = {}
let result = setDetails.call2(cat1, '大毛','橘色')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
将剩余参数传递给 context[uniqueName] 还可以通过 eval
来拼接调用语句
<script>
function setDetails(name, color) {
this.name = name;
this.color = color;
}
Function.prototype.call2 = function (context) {
function mySymbol(obj) {
let unique = Math.random + new Date();
if (obj.hasOwnProperty(unique)) {
return mySymbol(obj);
} else {
return unique;
}
}
let uniqueName = mySymbol(context);
// 获取除了第一个参数外剩余的参数
let args = [];
for (let i = 1; i < arguments.length; i++) {
args.push("arguments[" + i + "]");
}
// 将原函数作为 cat1 的方法调用
context[uniqueName] = this;
// 使用扩展运算符传参, 可以解决参数不确定的问题
let result = eval("context[uniqueName](" + args.join(",") + ")");
// 删除临时方法
delete context[uniqueName];
return result;
};
let cat1 = {};
let result = setDetails.call2(cat1, "大毛", "橘色");
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 实现apply
apply与call功能一致, 但是在调用方式上, 是将剩余的实参以一个数组的方式传参:
实现与call基本一致, 注意兼容 apply
第二个参数没有传入的情况:
使用扩展运算符
<script>
function setDetails(name, color) {
this.name = name;
this.color = color;
}
Function.prototype.apply2 = function (context, args) {
function mySymbol(obj) {
let unique = Math.random + new Date();
if (obj.hasOwnProperty(unique)) {
return mySymbol(obj);
} else {
return unique;
}
}
let uniqueName = mySymbol(context);
// 获取除了第一个参数外剩余的参数
let arr = [];
if (args) {
for (let i = 0; i < args.length; i++) {
arr.push("args[" + i + "]");
}
}
// 将原函数作为 cat1 的方法调用
context[uniqueName] = this;
// 使用扩展运算符传参, 可以解决参数不确定的问题
let result = eval("context[uniqueName](" + arr.join(",") + ")");
// 删除临时方法
delete context[uniqueName];
return result;
};
let cat1 = {};
let result = setDetails.apply2(cat1, ["大毛", "橘色"]);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 实现bind
bind
与 call
和 apply
的功能类似, 但有不同, bind
不会立即调用函数, 只做this
的绑定, 并且返回一个新的函数, 这个函数运行的逻辑与原函数一致, 但是this
会指向之前绑定的对象
function setDetails(name, color) {
this.name = name
this.color = color
}
let cat1 = {}
let setDetails2 = setDetails.bind(cat1)
setDetails2('大毛', '橘色')
2
3
4
5
6
7
实现思路: bind返回一个函数, 在函数体内调用 apply
Function.prototype.bind2 = function (context) {
let originFn = this;
return function () {
originFn.apply(context);
};
};
2
3
4
5
6
继续改进:
调用 bind 后返回的函数是可以传参的
bind方法除了第一个参数, 还可以额外传参, 可以理解为预传参
function setDetails3(name, color) {
this.name = name
this.color = color
}
let cat2 = {}
let setDetails4 = setDetails3.bind(cat2,'大毛')
setDetails4('橘色')
2
3
4
5
6
7
将第一次传参先存起来, 在调用将第一次和第二次传参进行拼接
Function.prototype.bind2 = function (context) {
let originFn = this;
let args = Array.from(arguments).slice(1)
return function () {
let bindArgs = Array.from(arguments)
return originFn.apply(context, args.concat(bindArgs));
};
};
function setDetails(name, color) {
this.name = name;
this.color = color;
return 123
}
let cat1 = {};
let setDetails2 = setDetails.bind2(cat1, '大毛');
let result = setDetails2('橘色');
console.log(result) // 123
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
最后改进:
bind
返回的函数, 如果之后是作为构造函数调用, 则原函数中的this
会指向创建的对象, 而不会指向之前绑定的对象, 并且生成的实例仍然可以使用原型对象上的属性
function Cat(name, color) {
this.name = name
this.color = color
}
Cat.prototype.miao = function() {
console.log('瞄~!')
}
let cat1 = {}
let CatBind = Cat.bind(cat1)
let cat2 = new CatBind('大毛','橘色')
cat2.miao()
2
3
4
5
6
7
8
9
10
11
当返回的函数作为构造函数, 通过 new xxx()
调用时, 返回的函数的this
指向并返回函数作为构造生成的类例, 因此需要判断this instanceof 返回的函数,
另外, 返回的函数要继承原函数 (将原型链连接起来)
<script>
Function.prototype.bind2 = function (context) {
let originFn = this;
let args = Array.from(arguments).slice(1);
function fBind() {
let bindArgs = Array.from(arguments);
return originFn.apply(this instanceof fBind /*是否作为构造函数调用*/ ? this:context, args.concat(bindArgs));
};
fBind.prototype = Object.create(this.prototype)
return fBind
};
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.miao = function () {
console.log("瞄~!");
};
let cat2 = {};
let CatBind = Cat.bind2(cat2);
let cat3 = new CatBind("大毛", "橘色");
cat3.miao();
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
实现new →