前言
春天到了,又到了交配,啊 ,不是。。又到了找工作的季节。相信很多朋友都会被问到过这样的一个JS问题,如何实现call
| apply
| bind
,很多朋友只会用但是不会写,或者是死记硬背写法,等到面试官提问的时候,支支吾吾讲不清楚,今天我将教会大家完全理解这个破题!
1.首先讲讲this
这是一个很方便,但是同时又容易出错的属性。
我们只要记住4条规则就好了
1.1 纯粹的函数调用
这个时候this指向window对象
var x = 'window';
function test() {
let x = 'fn';
console.log(this.x);
}
test(); // window
//直接调用的时候 this指向外部的window
向上面这种简单的大家都能理解,看看这个容易搞错的
var name = "zhangsan";
var obj = {
name:"leelei",
fn:function() {
var x = function() { console.log(this.name) };
x();
}
}
obj.fn() // zhangsan
1.2 作为对象的方法调用
这个时候的this就指向这个对象
function test() {
console.log(this.x);
}
var obj = {};
o.x = 1;
o.m = test;
o.m(); // 1
1.3 作为构造函数调用
function Test() {
this.x = 1;
}
var o = new Test();
console.log(o.x); // 1
1.4 apply,call,bind调用
apply(),call()是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this指的就是第一个参数。
bind()和他们类似,但是它执行后返回的还是一个函数,而不是执行后的值。this指的也是第一个参数。
2.实现call,apply
2.1 我们现来看看怎么使用
他的特性是把fn中的this指向第一个参数,当我们使用的时候是这样的。
它实现了把 sayName中的this指向了 obj,即this.nickName
=>obj.nickName
function sayName() {
console.log(`my name is ${this.nickName}`);
}
let obj = {nickName:"leelei"}
sayName.call(obj) //my name is leelei
2.2 原理是什么?
我们可以看看上面this的使用方法中的第二点,我们如果把fn设置为context的一个属性
,是不是fn的this就会指向context了呢?
context.property = fn;
let result = context.property();
delete context.property ;
return result;
- 先把fn设置为
context
的一个属性 - 然后执行这个方法,得到结果
result
- 然后删除这个属性,如果不删除,我们就污染了我们传入的这个
context
对象,谁乐意干干净净进来,出去的时候带了一坨屎啊。 - 返回结果,完美
2.3 参数又怎么搞阿?
call的用法是这样的:除了第一个参数以外,其他的参数全都是传给fn
那么借助es6语法我们可以省下一大堆代码,大致代码如下
Function.prototype.mycall\= function(context,...args) {
context.fn= this; //这里的this指向调用该方法的实例,也就是fn.call()中的fn
let result = context.fn(...args);
delete context.fn;
return result;
}
完事了吗?
当然没有,作为男人怎么可以那么快完事儿?
2.4 如果我们call方法传入的第一个参数不是对象,那又如何对敌?
想想knight
会怎么做?阿,不是,想想call会怎么做。
function sayName() {
console.log(`my name is ${this.nickName}`);
}
var sym = Symbol('halo') //ES6新增基础类型,如果不懂,没有瓜西!
sayName.call(sym) // my name is undefined
sayName.call('malegeji') // my name is undefined
sayName.call(666) // my name is undefined
sayName.call(true) // my name is undefined
sayName.call(null) // my name is undefined
sayName.call(undefined) //my name isundefined
undefined
代表什么呢?
你可以看看下面这个代码
//对一个对象访问它没有的属性值时会返回undefined
var obj = {};
obj.malegeji //undefined
这个说明call内部,把我们输入的基础类型都转成了对象,那么null和undefined也是如此吗?他们根本就没有自己的构造函数方法阿?那他们转成了什么?
是洋葱
,转成了洋葱
!
好吧,其实是window
如何验证?我们只要给个window的这个属性赋值看一下就知道啦
window.nickName = "leelei"
function sayName() {
console.log(\`my name is ${this.nickName}\`);
}
var sym = Symbol('halo')
sayName.call(sym) // my name is undefined
sayName.call('malegeji') // my name is undefined
sayName.call(666) // my name is undefined
sayName.call(true) // my name is undefined
sayName.call(null) // my name is leelei
sayName.call(undefined) //my name isleelei
哦豁,验证了我们的想法~
搞清楚特性以后,我们现在就可以写出一个和call一毛一样表现的mycall了~
Function.prototype.mycall = function(context,...args) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
if(context == null || context == undefined) {
context = window
}else{
context = Object(context);
}
context.fn = this;
let result = context.fn(...args);
delete context.fn;
return result;
};
3. 实现apply
apply和call其实大部分是一样的,他们的唯一区别是什么?
传参格式不一样
fn.call(context,arg1,arg2,arg3,...)
fn.apply(context,[arg1,arg2,arg3,...])
那么,我们可以轻易地实现apply
Function.prototype.myapply = function(context,args) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
if(arguments.length>2){
throw new Error("Incorrect parameter length")
}
if(context == null || context == undefined){
context = window
}else{
context = Object(context);
}
context.fn = this;
let result = context.fn(...args);
delete context.fn;
return result;
}
4. 实现bind
看这一部分之前,请先对构造函数
有一个比较清晰的了解,不然可以点赞
然后关掉网页了,当然也可再点个收藏
。
4.1 怎么使用?
function sayName(age,sex) {
console.log(`my name is ${this.nickName},I'm ${age} years old, ${sex}`);
}
let obj = { nickName: "leelei" }
let bindFn = sayName.bind(obj) //注意:使用bind后返回的是一个函数
bindFn(); //my name is leelei,I'm undefined years old, undefined
哎呀,忘了传参数,怎么传呢?
第一种
let bindFn = sayName.bind(obj,18,'man') //注意:使用bind后返回的是一个函数
bindFn(); //my name is leelei,I'm 18years old,man
第二种
let bindFn = sayName.bind(obj,18) //注意:使用bind后返回的是一个函数
bindFn('man'); //my name is leelei,I'm 18years old,man
你可以把除了第一个参数以外的参数随意在绑定的时候传入,或者在执行的时候传入,这个也是一个函数柯里化
的过程。
柯里化,英语:Currying是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
因为bind返回的是一个函数,当我们把这个函数当作构造函数
来使用,那又会怎样呢?
//为什么我一用构造函数举例会下意识命名为foo | bar
function Foo(age, sex) {
this.blog = "http://www.leelei.info"
console.log(this.nickName);
console.log(age);
console.log(sex);
}
Foo.prototype.habit = "play lol in Zu'an and kill somebody's mom";
let bindFn = Foo.bind({ nickName: "leelei" }, 18, "man");
let bindFnInstance = new bindFn(); // undefined 18 'man'
console.log(bindFnInstance.blog); // http://www.leelei.info
console.log(bindFnInstance.habit); //play lol in Zu'an and kill somebody's mom
let bindFnInstance2 = bindFn(); //普通调用,因为不是new运算符所以没有返回
console.log(bindFnInstance2.habit); // Cannot read property 'habit' of undefined
聪明的盲生,你发现什么华点了吗?
我的nickName
怎么是undefined
阿,完了,全完了,我浏览器有问题,我先把谷歌卸了!
别急,其实是因为当使用new操作符来构造绑定函数的时候,bind会忽略这个传入的第一个参数,为什么?
因为构造函数Foo中的this会指向实例用于构造实例,(这个是new的特性,如果不明白可以百度一下),那么this指到实例bindFnInstance后就不能指到传入的第一个参数了,那么它的nickName就是bindFnInstance的nickName了,但是bindFnInstance说:”我他妈刚生成哪里来的nickName阿“,所以最终就无法访问了嗷。
好的,我们来总结一下这几个特性嗷
- 返回函数
- 柯里化
- new构造的时候忽略传入的上下文对象,把this指向生成的实例
4.2 先实现第一个返回函数
特性
那颗太简单了嗷,铁子,干了奥里给!
Function.prototype.mybind = function(context) {
return function() {
return fn.call(context);
};
};
4.3 再来实现这个参数的分步传入
Function.prototype.mybind = function(context, ...args) {
return function(...args2) {
return fn.call( context, ...args, ...args2);
};
};
4.4 最后来实现这个new的指向判断
Function.prototype.mybind = function(context, ...args) {
const fn = this;
function fBound(...args2) {
return fn.call(this instanceof fBound ? this : context, ...args, ...args2);
};
fBound.prototype = Object.create(this.prototype);
return fBound;
};
this instanceof fBound? this : context
是干什么的?
你可能看过如何判断数组代码,arr instanceof Array
,是不是感觉很像?有么有感觉了?
这个instanceof
可以判断 右边这个构造函数是否出现在左边这个对象的原型链上。
按照写法,我们返回了fBound
- 如果使用普通调用,那么我们这个this会指向window嘛(fBound中的this属于第一部分提到的this的第一种用法),window和fBound有个毛关系?所以返回context作为fn的第一个参数。
- 如果使用的是new,那么这个this指向的就是新的实例,新的实例的原型链肯定有它的构造函数fBound阿,那么就传入this,也就是实例本身,而忽略context,其他参数不变。
fBound.prototype = Object.create(this.prototype)
是干什么的?
当我们使用构造函数的时候,构造函数原型上的属性,实例也可访问,也就是这里所表现的。
Foo.prototype.habit = "play lol in Zu'an and kill somebody's mom";
console.log(bindFnInstance.habit); //play lol in Zu'an and kill somebody's mom
但是我们返回的是fBound,fBound哪里来的prototype.habit阿,所以我们给他整上!
那能不能直接执行fBound.prototype =fn.prototype
,将原函数的 prototype 赋值给 fBound 呢?
很明显这样的操作把 fBound 和 原函数的 prototype 强关联起来了,如果fBound 函数的 prototype改动 将会影响到原函数的 prototype,所以可以通过 fBound.prototype = Object.create(fn.prototype)
,以原函数的 prototype为模板,生成一个新的实例对象,并赋值给fBound.prototype。
4.5 完事了吗?
当然没有!
还有一个需要注意的点
当我们执行到 fBound.prototype = Object.create(fn.prototype)
时,如果fn.prototype是undefined可咋整,什么情况下会出现呢?
当我们直接调用 Foo.prototype.bind 时候会出现,并且bong
的一声报了个大错!
typeof Function.prototype === "function" //true
所以我们像前面call,apply那样的判断也限制不了 同时
Function.prototype.prototype // undefined
4.6 完事儿了吗?
当然。。。没有!
因为 Object.create() 和 bind 都是 ES5 规范提出的,如果不支持 bind, 那么bind 的 polyfill 里面自然不支持 Object.create()。所以我们应该换个方法来实现,一般面试官到上面一步就足了。
最终究极终稿!
Function.prototype.mybind = function(context, ...args) {
if (typeof this !== "function") {
throw new TypeError("not funciton");
}
const fn = this;
const fNop = function () {};
function fBound(...args2) {
return fn.call(this instanceof fBound ? this : context, ...args, ...args2);
};
if(fn.prototype){
fNop.prototype =fn.prototype;
}
fBound.prototype = new fNop();
return fBound;
};
5. 总结
总的来说call,apply,bind这三个方法涉及到了js的诸多方法,如果能够完全理解的话,对于学习js会有很大帮助嗷~
如果有错误,请在评论区中指出,非常感谢!
顺便打个广告 leelei的个人博客
最后祝大家拿到心仪的offer
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。