写在前面:看似简单的一个JS的小方法,但从不断的挖掘中扩展了知识的深度和广度,除此之外,也是练习并掌握了一个学习方法,提高学习能力。从使用到理解,再到尝试实现,不断去试错,重复去阅读,最后把脑中的理解转换成文字。
参考文章:
https://segmentfault.com/a/11...
https://juejin.im/post/5bec41...
http://yanhaijing.com/es5/#238
https://segmentfault.com/q/10...
https://blog.csdn.net/wxl1555...
https://www.w3cschool.cn/java...
https://www.bbsmax.com/A/lk5a...
1. call方法
1.1 概述
参数解释:
function.call(thisArg, arg1, arg2, ...)
;
thisArg
:在fun函数运行时指定的this
值。需要注意的是,指定的this
值并不一定是该函数执行时真正的this
值,如果这个函数处于非严格模式下,则指定为null
和undefined
的this
值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this
会指向该原始值的自动包装对象。arg1, arg2, ...
:指定的参数列表。
var obj = { name:'CSDN' };
function fn() {
// console.log(this);
console.log(this.name);
}
fn(); // undefined
fn.call(obj); // CSDN
理解:首先寻找call
方法,通过原型链的查找,在Function.prototype
上找到call
方法;然后,改变fn
函数中的this
指向,将fn
执行。
var obj = { name:'zl' };
function fn(age, country) {
console.log(this.name + '-' + age + '-' + country);
}
fn.call(obj, 18, 'China'); // zl-19-China
理解:带参数传入,参数需要展开,这也是唯一于apply
方法不同的地方。
1.2 模拟call方法的原理去理解this
var obj = { age: 18 }
function fn1 () {
console.log(this);
console.log('fn1');
}
Function.prototype.myCall = function (arguments) {
// 1. 解构传递进来的参数arguments
// obj(需要改变的目标指向) arg1,arg2,arg3..(其他参数)
// 2. 改变fn1函数中的this指向obj
// 3. 此函数体(myCall)中的this指向fn1
// 将arg1,arg2..(其他参数)传入fn1方法中执行
}
fn1.myCall(obj, arg1, arg2, arg3...);
// myCall方法中原来的this是obj
// 输出: { age: 18 } fn1
fn1.myCall(obj)
在执行的时候,首先改变的是fn1
函数中的this
指向为目标对象,而myCall
方法中的this
是指向fn1
的。所以,最后执行的时候也是执行myCall
方法中的this
所指向的函数。
var obj = {};
var f = function () {
return this;
};
f() === this // true
f.call(obj) === obj // true
上面代码中,在全局环境运行f
时,this
指向全局环境;call
方法可以改变this
的指向,指定this
指向对象obj
,然后在对象obj
的作用域中运行函数f
。
function f1 () {
console.log('f1')
// console.log(this) 输出 [Function: f2]
}
function f2 () {
console.log('f2')
}
f1.call(f2) // f1
上例再次很好的说明了:call
方法只会改变f1
的this
指向,而不是改变自身的this
指向,最后执行的函数,只会是自身的this
指向——调用call
的f1
,而不是f2
。
1.3 总结
call
方法的第一个参数用于改变调用call
方法的函数内,this
的指向,但是如果传入null/undefined
值,此this
会指向window
call
方法需要把实参按照形参的个数传进去call
方法最后会使用参数去执行call
函数体内this
所指向的函数,一般是指向调用call
的函数
1.4 深入理解call的小例子
function f1 () {
console.log('f1')
}
function f2 () {
console.log('f2')
}
function f3 () {
console.log('f2')
}
f1.call(f2) // f1
f1.call.call(f2) // f2
f1.call.call.call(f2) // f2
f1.call.call.call(f3) // f3
要理解f1.call.call(f2)
,首先拆分它,先看f1.call
,在Function.prototype
上找到call
方法,只实现——call
函数体中的this
指向f1(没有需要f1
中this
改变的目标对象)。所以现在可以将f1.call
看成一个函数(func),再次强调此函数中的this
指向f1
。
// f1.call 可以写成如下
Function.prototype.call = function (obj) {
// 1. 没有改变f1中this的指向
// 2. 此函数体内this指向f1
this() // 指向f1
}
Func = Function.prototype.call
// function Func () {
// ... 省略代码
this() //指向f1
}
现在f1.call.call(f2)
可以看成是func.call(f2)
。func
调用call
方法,首先改变func
中this
的指向为f2
,然后执行调用了call
的函数func
(func
就是之前的f1.call
),而这个函数内,this
指向f2
,所以这里输出‘f2’
。
Function.prototype.call = function (obj) {
// 1. 改变Func中this的指向为f2
// 2. 执行this()
this() // 指向Func
}
其他例子可照此法去理解,能够明白call
改变的是谁的this
,最后执行的又是哪个函数。
1.5 动手仿写一个call方法
之前我们已经知道了call
内部都做了什么,接下来通过代码来实现它的功能:
call
方法接收的第一个参数应该是一个对象,非严格模式下,如果为空、null、undefined,则默认传入全局对象。call
方法改变函数内部的this
指向,并在指定作用域中调用该函数
// node 环境下输入 love = 'global love'
// window 浏览器中运行输入 var love = 'window love'
var love = 'window love'
Function.prototype.myCall = function () {
// console.log(arguments);
// 输出:{'0': Object { age: 19, love: "sleeping" }, '1': 23, '2': 25, ...}
var [thisArg, ...args] = [...arguments]
if(!thisArg) {
thisArg = typeof window === undefined ? global : window
}
thisArg.fn = this
let res = thisArg.fn(...args)
delete thisArg.fn
return res
}
var zhou = {
age: 18,
love: 'coding',
hello: function (age) {
console.log("hello world, i am zhou," + age + "," + this.love);
}
};
var wang= {
age: 19,
love: 'sleeping'
};
zhou.hello.call();
// hello world, i am zhou,undefined window love
zhou.hello.myCall(wang, 23, 25, 90, 8)
// hello world, i am zhou,23 sleeping
从代码log(arguments)
中,我们知道了myCall
方法中arguments
的结构:第0个元素对象正是我们需要修改this
的目标对象,其他元素则是传递的实参。if
语句是为了更好的完善myCall
的功能,根据代码运行环境,全局对象的this
的指向是不同的,具体参考此文。
关键是thisArg.fn
的理解:myCall
中的this
指向的是zhou.hello
这个方法;给thisArg
添加一个fn
的属性(就是方法zhou.hello
)。而thisArg.fn(...args)
可以看成zhou.hello(23, 25, 90, 8)
,而zhou.hello
只接受一个参数,自然就是第一个23了。最后通过delete
删除自添加的属性this.fn
。
1.6 补充
了解了call
方法的作用原理,最后看一个es5的关于把类数组转换为数组的方法:Array.prototype.slice.call(arguments)
function test () {
var res = Array.prototype.slice.call(arguments, 1,3)
console.log(res); // [ 'bbb', 123 ]
}
var a = 'aaa', b = 'bbb', c = 123, d = 'ddd'
test(a,b,c,d)
slice
是数组才有的切割数组的方法,arguments
是典型的类数组对象。这里先是调用Array.prototype
上的slice
方法,而这个方法肯定是有call
方法的,根据其作用原理,将Array.prototype.slice
的作用域改为arguments
,最后执行返回一个数组。
2. apply方法
2.1 概述
调用一个具有给定this
值的函数,以及作为一个数组(或类似数组对象)提供的参数。
func.apply(thisArg, [argsArray])
thisArg
: 可选的。在func
函数运行时使用的this
值。请注意,this
可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为null
或undefined
时会自动替换为指向全局对象,原始值会被包装。argsArray
:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func
函数。如果该参数的值为null
或undefined
,则表示不需要传入任何参数。
2.2 举例说明call和apply
// 简单的
let arr = [1, 2, 3]
let obj = {
name: 'obj inner'
}
function test (one, two, three) {
console.log(one, two, three)
console.log(arguments)
return this.name
}
console.log(test.apply(obj, arr))
// 1 2 3
// [Arguments] { '0': 1, '1': 2, '2': 3 }
// obj inner
//求数组中的最大值
var arr = [2, 7, 10, 1]
function getMax2(arr) {
return Math.max.apply(null, arr);
//return Math.max.call(null, ...arr);
}
console.log(getMax2(arr)); //10
alert(Math.max(1,4,9,6)); //9
// 实现继承
function Animal(type, value) {
this.type = type;
this.value = value;
}
function Dog(type, value) {
Animal.apply(this, [type, value]);
// Animal.call(this, type, value)
this.name = "二狗子";
this.age = 18
}
var hehe = new Dog("室友", "1");
console.log(hehe.name); //二狗子
console.log(hehe.type); //室友
总结
apply
方法接收到的数组作为参数传递给func
时,需要用多个参数来接受数组中的每一项( 猜测Math.max.apply(null, arr)
中apply
会自动将数组转变成参数列表,其等价于Math.max(2,7,10,1)
)。可使用arguments
来全部接收。- 我们可以看出
apply
和call
的不同:接收的参数一个是数组一个是参数列表。 - 可以使用
apply
和call
实现继承,方法是类似的,结果是一致的。
apply
还多用于构造函数绑定:链接
2.3 实现apply方法
Function.prototype.apply = function () {
var [thisArg, args] = [...arguments]
// 与call方法的实现相类似,运用展开运算
if(!thisArg) {
thisArg = typeof window === undefined ? global : window
}
thisArg.fn = this
let res = thisArg.fn(...args)
delete thisArg.fn
return res
}
3. bind方法
3.1 概述
返回一个原函数的拷贝(也称绑定函数),在调用时设置this
关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。详见MDN。
function.bind(thisArg[, arg1[, arg2[, ...]]])
thisArg
:调用绑定函数时作为this
参数传递给目标函数的值。 如果使用new
运算符构造绑定函数,则忽略该值。当使用bind
在setTimeout
中创建一个函数(作为回调提供)时,作为thisArg
传递的任何原始值都将转换为object
。如果bind
函数的参数列表为空,执行作用域的this
将被视为新函数的thisArg
。arg1, arg2, ...
:当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。
举例说明:
window.value = 3;
var foo = {
value:1
};
function bar() {
console.log(this.value);
}
bar(); // 3
bar.call(foo); //1
//指定函数this绑定为foo, 产生一个新函数,之后再运行的时候,内部的this就是被绑定的对象
var bindFoo = bar.bind(foo);
setTimeout( function() {
bindFoo();
},2000)
// 2秒后打印 1
这个例子可以很好的理解bind
的运用:
bar()
直接调用函数,其中的value
指的是全部变量value = 3
bar.call(foo)
这里使用call
立刻改变了bar
中的this
指向为foo
中bind
常用于异步,在setTimeout
中,设置的时间内,bar
的this
保留着指向foo
,所以两秒后打印1,不是3。
3.2 call,apply与bind的不同
bind()
可以为目标函数保留this
的指向,当执行目标函数时,this
会指向设置的作用域。call()
方法会立即执行!bind()
方法会返回函数的拷贝值,但带有绑定的上下文! 需要我们手动调用执行。
3.3 注意
bind()
方法永久改变this
的指向,后面再用call()
会失效(对应上一小节的第一点):
var name='rose';
var obj={
name:'jack',
func:function(){
console.log(this.name)
}
}
var obj2={ name:'zl' }
var func=obj.func.bind(obj);
func.call(obj2); // 无法改变this指向
// jack
3.4 实现bind方法
先看一个例子,明白bind的用法中的难点:
var obj = {
name: 'out obj',
};
function original(a, b){
console.log('this', this); // original {}
console.log('typeof this', typeof this); // object
this.name = b
console.log('name', this.name); // name 2
console.log('this', this); // original {name: 2}
console.log([a, b]); // 1, 2
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // original {name: 2}
从上例中可以了解,由于使用new
,bind
原来实现original
的this
指向obj
失效了。new bound
的返回值,可以理解为是以origin
为原型生成的新对象。而根据new
的功能,original
中的this
指向的就是这个新对象。
new
的功能:
- 创建一个空对象,构造函数中的this指向这个空对象
- 这个新对象被执行[原型]连接
- 执行这个构造函数属性和方法添加到this新对象中
- 如果构造函数中没有返回其他对象,那么就返回this,即创建的新对象
MDN:绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。
说明绑定函数被new
实例化之后,需要继承原函数的原型链方法,且绑定过程中提供的this被忽略(继承原函数的this对象),但是参数还是会使用。所以需要一个中转函数把原型链传递下去。即最终实例化之后的对象this
需要继承自原函数
再看我们的mybind
方法:
Function.prototype.mybind = function () {
if (typeof this !== "function") {
throw new TypeError(this + 'must be a function');
}
let _this = this
var [thisArg, ...args] = [...arguments]
function fn () {
let tempargs = [...arguments]
let newargs = args.concat(tempargs)
_this.apply(thisArg, newargs)
}
return fn
返回的函数的this
指向是固定的,在执行mybind
的时候就已经固定是thisArg
,并没有把原函数的this
对象继承过来。
所以在new
新的实例的时候实时将这个新的this
对象 进行 apply
继承原函数的 this
对象,
Function.prototype.mybind = function () {
if (typeof this !== "function") {
throw new TypeError(this + 'must be a function');
}
let _this = this
var [thisArg, ...args] = [...arguments]
let fTemp = function () {}
function bound () {
let tempargs = [...arguments]
let newargs = args.concat(tempargs)
_this.apply(this instanceof fTemp ? this : that || window, newargs)
}
fTemp.prototype = _this.prototype
bound.prototype = new fTemp()
return bound
}
var zhou = {
age: 18,
love: 'coding',
hello: function (age,a,b,c,d,e) {
console.log("hello world, i am zhou," + age + "," + this.love);
console.log(a,b,c,d,e);
}
};
var wang= {
age: 19,
love: 'sleeping'
};
let bound = zhou.hello.mybind(wang, 23, 25, 90, 8)
let a = new bound()
console.log(a); // hello {}
}
重点理解这部分:
....
let fTemp = function () {}
function bound () {
let tempargs = [...arguments]
let newargs = args.concat(tempargs)
_this.apply(this instanceof fTemp ? this : that || window, newargs)
}
fTemp.prototype = _this.prototype
bound.prototype = new fTemp()
return bound
...
这里需要是区分 bound
是直接调用还是被new
之后再调用(mybind
返回的就是bound
),通过原型链的继承关系可以知道,boud
属于 after_new
(new
出来的实例)的父类,所以 after_new instanceof bound
为 true。
同时fTemp.prototype = _this.prototype
和bound.prototype = new fTemp()
原型继承, 使得 fTemp
也是 after_new
的父类, after_new instanceof fTemp
为 true。
最后,因为let after_new = new bound()
使得bound
中的this
指向的就是after_new
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。