在模拟实现 call、apply 和 bind 方法以及 new 操作符的过程中,能让我们更好的理解他们的运行原理。
1、区别
call 、apply 和 bind 方法都是为了解决改变 this 的指向。
call 、apply 方法都是在传入一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。作用都是相同的,只是传参的形式不同。
除了第一个参数外,call 接收一个参数列表,apply 接受一个参数数组。使用 call 、apply 方法该函数会被执行。
bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数。即使用 bind 方法该函数不会被执行(返回该函数)。并且我们可以通过 bind 实现柯里化。
let a = { value: 1}function getValue(name, age) { console.log(name, age) console.log(this.value) } getValue.call(a, 'xql', '18') getValue.apply(a, ['xql', '18']) getValue.bind(a, 'xql', '18') // getValue.bind(a, 'xql', '18')() 执行该函数
2、模拟实现 call 和 apply
先看看call 和 apply 方法发生了什么?
改变了 this 的指向,指向到 a
getValue 函数执行了
该怎么模拟实现这两个效果呢?
既然 call 和 apply 方法是改变了 this 指向,让新的对象可以执行该函数,那么思路是否可以变成给新的对象添加一个函数(将函数设为对象的属性),然后在执行完以后删除?
Function.prototype.myCall = function(objCtx) { if (typeof this !== 'function') { throw new TypeError('Error') } let ctx = objCtx || window; let args = [...arguments].slice(1); // let args = Array.prototype.slice.call(arguments, 1) ctx.fn = this; let result = ctx.fn(...args); delete ctx.fn; return result; // 函数是可以有返回值的}// 测试一下let a = { value: 1}function getValue(name, age) { console.log(this.value) return { name: name, age: age, value: this.value } } getValue.myCall(a, 'xql', '19') getValue.myCall(null)console.log(a)
fn 是对象的属性名,因为最后要delete,这里可以是任意名称。
以上就是 call 方法的思路,apply 方法的实现也类似。
Function.prototype.myApply = function(objCtx) { if (typeof this !== 'function') { throw new TypeError('Error') } let ctx = objCtx || window; ctx.fn = this; // 属性名fn可以任意 let result; // 需要判断是否存在第二个参数 if (arguments[1]) { result = ctx.fn(...arguments[1]); } else { result = ctx.fn(); } delete ctx.fn; return result; }
3、bind 方法的模拟实现
bind 函数的两个特点?
可以传入参数
返回一个函数
① 其中,对于 bind 函数中的传入参数有个特点
既然在使用 bind 函数的时候,是可以传参的。那么,在执行 bind 返回的函数的时候,可不可以传参呢? 类似函数柯里化。
let foo = { value: 1};function getValue(name, age) { console.log(this.value); console.log(name, age); }let bindFoo = getValue.bind(foo, 'xql'); bindFoo('18');
② 另外,因为 bind 函数返回一个新函数,所以 bind 函数还有一个特点
当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数(调用 bind 方法的函数)当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name, age); } bar.prototype.friend = 'shuaige'; let bindFoo = bar.bind(foo, 'xql'); var obj = new bindFoo('18'); // undefined 这里对 bind 函数绑定的 this 失效了 // xql 18 console.log(obj.habit); console.log(obj.friend);
注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,这里可以查看 new 的模拟实现,就会知道这个时候的 this 已经指向了 obj。
实现步骤
首先实现传参;
Function.prototype.myBind = function (objCtx) { let ctx = objCtx || window; let _this = this; let args = [...arguments].slice(1); return function () { // 这里的 arguments 与上面的 arguments 的值是不同的 这里是对返回函数所传入的参数 下面是将类数组 arguments 转为数组的两种方式 这里在使用的时候要记得转化为数组 // args.concat(arguments) 这样使用会出现问题 arguments 是个类数组对象 // let bindArgs = [...arguments] // let bindArgs = Array.prototype.slice.call(arguments) return _this.apply(ctx, args.concat(...arguments)) // 为什么要写return 因为绑定函数(函数)是可以有返回值的 } }// 测试一下 var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name, age); } let bindFoo = bar.myBind(foo, 'xql'); bindFoo('1111')
其次,实现绑定函数(调用 bind 方法的函数)作为构造函数进行使用的情况;
Function.prototype.myBind = function (objCtx) { let ctx = objCtx || window; let _this = this; let args = [...arguments].slice(1); let Fbind = function () { // 当 Fbind 作为构造函数时,这里的 this 指向实例 let self = this instanceof Fbind ? this : ctx; return _this.apply(self, args.concat(...arguments)); } Fbind.prototype = this.prototype; // // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值 // 不要写成 Fbind.prototype = ctx.prototype; return Fbind; }
这里,我们直接将 Fbind.prototype = this.prototype
,会导致修改 Fbind.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转( js 高程中成为原型式继承)。
最终的实现为:
Function.prototype.myBind = function (objCtx) { if (typeof this !== 'function') { throw new TypeError('Error'); } let ctx = objCtx || window; let _this = this; let args = [...arguments].slice(1); let Fbind = function () { let self = this instanceof Fbind ? this : ctx; return _this.apply(self, args.concat(...arguments)); // 这里可以使用 call 方法 // let bindArgs = args.concat(...arguments); // return _this.call(self, ...bindArgs); } let f = function () {}; f.prototype = this.prototype; Fbind.prototype = new f(); return Fbind; } // 测试一下 var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); // undefined console.log(name, age); } bar.prototype.friend = 'shuaige'; var bindFoo = bar.myBind(foo, 'xql'); var obj = new bindFoo('18'); console.log(obj.habit); console.log(obj.friend);
其中,Object.create()
模拟实现为:(Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。 )
Object.create = function( o ) { function f(){} f.prototype = o; return new f; };
4、new操作符的模拟实现
通过 new 操作符的模拟实现,来看看使用 new 获得构造函数实例的原理。
先来看看使用 new 操作符发生了什么?
创建一个空对象;
该空对象的原型指向构造函数(链接原型):将构造函数的 prototype 赋值给对象的
__proto__
属性;绑定 this:将对象作为构造函数的 this 传进去,并执行该构造函数;
返回新对象:如果构造函数返回的是一个对象,则返回该对象;否则(若没有返回值或者返回基本类型),返回第一步中新创建的对象;
举例说明:
构造函数没有返回值的情况:
function Angel (name, age) { this.name = name; this.age = age; this.habit = 'Games'; } Angel.prototype.strength = 60; Angel.prototype.sayYourName = function () { console.log('I am ' + this.name); } var person = new Angel('xql', '18'); console.log(person.name, person.habit, person.strength) // xql Games 60 person.sayYourName(); // I am xql
构造函数有返回值的情况:返回对象 or 返回基本类型(返回 null )
function Angel (name, age) { this.strength = 60; this.age = age; return { name: name, habit: 'Games' } } var person = new Angel('xql', '18'); console.log(person.name, person.habit) // xql Games 60 console.log(person.strength, person.age) // undefined undefined
function Angel (name, age) { this.strength = 60; this.age = age; return '111' // 和没有返回值以及 return null 效果是一样的 } var person = new Angel('xql', '18'); console.log(person.name, person.habit) // undefined undefined console.log(person.strength, person.age) // 60 "18"
具体实现
因为 new 是关键字, 我们只能写一个函数来模拟了,构造函数作为参数传入。
// 使用 newvar person = new Angel(……);// 使用 objectFactoryvar person = objFactory(Angel, ……)
function objFactory () { let obj = {}; // let obj = new Object (); // 取出参数中的第一个参数即构造函数, 并将该参数从 arguments 中删掉 let Con = [].shift.call(arguments); // 不要写成 [].prototype.shift.call(arguments) obj._proto_ = Con.prototype; let result = Con.apply(obj, arguments); // let result = Con.apply(obj, [...arguments]); // let result = Con.call(obj, ...arguments) return Object.prototype.toString.call(result) === '[object Object]' ? result : obj; // 这里用typeof result == 'object' 进行判断会有个问题:当构造函数返回 null 时,会报错,因为 typeof null == 'object' // 应该是除了构造函数返回一个对象,其他的都返回新创建的对象} // 测试一下function Angel (name, age) { this.strength = 60; this.age = age; return null}var person = objFactory(Angel, 'Kevin', '18');console.log(person.name, person.habit); // undefined undefinedconsole.log(person.strength, person.age) // 60 "18"
3、 小知识点
对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。
对于 new 来说,还需要注意下运算符优先级。
function Foo() { return this; } Foo.getName = function () { console.log('1'); }; Foo.prototype.getName = function () { console.log('2'); };new Foo.getName(); // -> 1new Foo().getName(); // -> 2
作者:黎贝卡beka
链接:https://www.jianshu.com/p/097f995178e1