JavaScript学习之call、apply、bind的理解与实现
学习冴羽的JavaScript深入之call和apply的模拟实现和JavaScript深入之bind的模拟实现的笔记
call
fun.call(thisArg[,arg1[,arg2,…]]);
call
是属于所有Function
的方法,也就是Function.prototype.call
。- call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
- call() 方法可以指定若干个参数
使用call实现继承
function Animal( name,type )
{
this.name = name
this.type = type
}
function Cat( name,type,age )
{
Animal.call( this,name,type )
this.age = age
this.say = function ()
{
console.log( `i am ${ this.name }` );
}
}
var cat = new Cat( 'lily','miao',2 )
cat.say()
实现call()
第一步:实现改变this
//第一版
Function.prototype.call2 = function ( context )
{
// 首先要获取调用call的函数,用this可以获取
context.fn = this; // foo.fn = bar
context.fn(); // foo.fn()
delete context.fn;//删除 foo.fn 方法
}
//测试一下
var foo = {
value: 1
};
function bar()
{
console.log( this.value );
}
bar.call2( foo ); // 1
第二步:有参数怎么处理呢?
在执行call的时候是可以传入参数的,但是每次传的参数的个数都是不同的,那该怎么办呢?
这是我们可以使用
arguments
,从其中取参数
arguments 是一个对应于传递给函数的参数的类数组对象。
// 通过循环的方式获取arguments中的参数,并push到args中
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
现在 参数我们已经准备好了,怎么把它们传给 context.fn()
呢?
context.fn(args.join(,))
// 这种方式是不可以的!
那要怎么办呢? 我们可以通过额eval() 这个方法实现
eval() 是什么??
eval() 函数作用是:可计算某个字符串,并执行其中的的 JavaScript 代码。
所以 第二步实现来咯
// 第二版
Function.prototype.call2 = function(context) {
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args +')');
delete context.fn;
}
// 测试一下
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'kevin', 18);
// kevin
// 18
// 1
第三步:还有一些小问题
这是代码基本实现差不多了,但是还有两个小问题
- 当fn.call(null) 传过去的 this 是 null ,此时应视为指向的是window
- 当 fn 方法有返回值的时候
根据以上两个问题,我们的第三步来了!
Function.prototype.call2 = function ( context )
{
var context = context || window;
context.fn = this;
var args = [];
for ( var i = 1,len = arguments.length; i < len; i++ )
{
args.push( 'arguments[' + i + ']' );
}
// eval() 用于计算某个字符串并且执行计算完之后的代码
var result = eval( 'context.fn(' + args + ')' );
delete context.fn
return result;
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar( name,age )
{
console.log( this.value );
return {
value: this.value,
name: name,
age: age
}
}
function bar1()
{
console.log( this.value );
}
function bar2()
{
console.log( this.value );
}
console.log( bar.call2( obj,'kevin',18 ) );
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
console.log( bar1.call2( obj ) ); //1
bar2.call( null ); // 2
好了,到此为止 call 的实现完成了!!!
apply
apply 和 call 基本类似。
不同点是:apply只能传递this和一个参数,call 可以传递多个参数
实现 apply
Function.prototype.apply = function (context) {
var context = Object(context) || window;
context.fn = this;
var result = eval('context.fn(' + arguments[1] + ')')
delete context.fn
return result;
}
bind
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )
返回函数的实现
先举个例子
var foo = {
value: 1
};
function bar() {
console.log( this.value );
}
bar.bind( foo )()
bind() 方法 执行后会创建一个新函数并返回这个新函数,bind第一个参数 foo 会被当做新函数运行时的this,之后第参数会被认为是新函数的参数。
首先,需自己写一个apply或者call,不用call 和 apply 实现bind
简单版的实现bind 可以使用call 和 apply,我们在这里不用这两个方法实现 bind,这里写一个apply,因为apply的参数只能是一个,所以比较简单。
Function.prototype.apply1 = function ( context )
{
context.fn = this
var res = eval( `context.fn(${ arguments[ 1 ] })` )
console.log( this )
delete context.fn
return res
}
实现这一步
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
return self.apply1(context);
}
}
bind传参的实现
先举一个例子
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18
函数bar 需要两个参数,我们在执行bind的时候传递进去一个 name,然后在执行返回的函数bindFoo的时候也可以传参数!神奇!太神奇了!
那我们要怎么实现呢???
实现bind传参来了
var foo = {
value: 1
};
function bar( name, age )
{
console.log( this.value );
console.log( a,b,c )
}
//-------------------------------------------------------
Function.prototype.bind2 = function ( context )
{
var self = this;
var args = []
for ( let i = 1; i < arguments.length; i++ )
{
args.push( arguments[ i ] )
}
return function ()
{
return self.apply1( context,args.concat( [ ...arguments ] ) );
}
}
console.log( bar.bind2( foo,'daisy' )( 18 ) )
通过将执行bind() 时穿过去的参数与执行返回的函数传过去的参数相结合,然后调用apply1() 这样实现bind的参数传递。
构造函数效果的实现
最难的部分来咯!!!
bind方法返回的函数也可以作为一个构造函数
使用!
这时,bind方法传递给他的 this 就无效了,但是参数依然是有效地,而这时 this 指向的就是实例出来的对象了。
先举个例子
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'shopping';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'kevin';
var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo('18');
// undefined --->this 已经不指向 foo了
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
实现构造函数效果来了
Function.prototype.bind2 = function ( context )
{
var self = this;
var args = []
for ( let i = 1; i < arguments.length; i++ )
{
args.push( arguments[ i ] )
}
var myFun = function ()
{
// 当作为构造函数时,this 指向实例对象,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply1( this instanceof myFun ? this : context,args.concat( [ ...arguments ] ) );
}
myFun.prototype = this.prototype
return myFun
}
构造函数效果的优化
我们通过 myFun.prototype = this.prototype 修改返回函数的 prototype 为绑定函数的 prototype,这种方式虽然可以,但是一旦修改myFun的prototype 就会修改绑定函数的 prototype,会污染绑定函数的prototype
所以,优化一下:
Function.prototype.bind2 = function ( context )
{
var self = this;
var args = []
for ( let i = 1; i < arguments.length; i++ )
{
args.push( arguments[ i ] )
}
var funFather = function (){}
var myFun = function ()
{
// 当作为构造函数时,this 指向实例对象,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
return self.apply1( this instanceof funFather ? this : context,args.concat( [ ...arguments ] ) );
}
funFather.prototype = this.prototype
muFun.prototype = new funFather()
return myFun
}