春天到了,又到了交配,啊 ,不是。。又到了找工作的季节。相信很多朋友都会被问到过这样的一个JS问题,如何实现call | apply | bind,很多朋友只会用但是不会写,或者是死记硬背写法,等到面试官提问的时候,支支吾吾讲不清楚,今天我将教会大家完全理解这个破题!
这是一个很方便,但是同时又容易出错的属性。
我们只要记住4条规则就好了
这个时候this指向window对象
let x = 'window';
function test() {
let x = 'fn';
console.log(x);
}
test(); // window
//注意这里如果使用var的话,会是fn
//因为var没有块级作用域,函数内var会相当于在外面var
//也就是更改了window.x也就是this.x。
向上面这种简单的大家都能理解,看看这个容易搞错的
var name = "zhangsan";
var obj = {
name:"leelei",
fn:function() {
var x = function() { console.log(this.name) };
x();
}
}
obj.fn() // zhangsan
这个时候的this就指向这个对象
function test() {
console.log(this.x);
}
var obj = {};
o.x = 1;
o.m = test;
o.m(); // 1
function Test() {
this.x = 1;
}
var o = new Test();
console.log(o.x); // 1
apply(),call()是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this指的就是第一个参数。
bind()和他们类似,但是它执行后返回的还是一个函数,而不是执行后的值。this指的也是第一个参数。
他的特性是把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
我们可以看看上面this的使用方法中的第二点,我们如果把fn设置为context的一个属性,是不是fn的this就会指向context了呢?
context.property = fn;
let result = context.property();
delete context.property ;
return result;
call的用法是这样的:除了第一个参数以外,其他的参数全都是传给fn
Function.prototype.mycall\= function(context,...args) {
context.fn= this; //这里的this指向调用该方法的实例,也就是fn.call()中的fn
let result = context.fn(...args);
delete context.fn;
return result;
}
完事了吗?
当然没有,作为男人怎么可以那么快完事儿?
想想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;
};
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;
}
看这一部分之前,请先对构造函数有一个比较清晰的了解,不然可以点赞然后关掉网页了,当然也可再点个收藏。
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阿“,所以最终就无法访问了嗷。
好的,我们来总结一下这几个特性嗷
那颗太简单了嗷,铁子,干了奥里给!
Function.prototype.mybind = function(context) {
return function() {
return fn.call(context);
};
};
Function.prototype.mybind = function(context, ...args) {
return function(...args2) {
return fn.call( context, ...args, ...args2);
};
};
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;
};
你可能看过如何判断数组代码,arr instanceof Array,是不是感觉很像?有么有感觉了?
这个instanceof 可以判断 右边这个构造函数是否出现在左边这个对象的原型链上。
按照写法,我们返回了fBound
- 如果使用普通调用,那么我们这个this会指向window嘛(fBound中的this属于第一部分提到的this的第一种用法),window和fBound有个毛关系?所以返回context作为fn的第一个参数。
- 如果使用的是new,那么这个this指向的就是新的实例,新的实例的原型链肯定有它的构造函数fBound阿,那么就传入this,也就是实例本身,而忽略context,其他参数不变。
当我们使用构造函数的时候,构造函数原型上的属性,实例也可访问,也就是这里所表现的。
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。
当然没有!
还有一个需要注意的点
当我们执行到 fBound.prototype = Object.create(fn.prototype) 时,如果fn.prototype是undefined可咋整,什么情况下会出现呢?
当我们直接调用 Foo.prototype.bind 时候会出现,并且bong的一声报了个大错!
typeof Function.prototype === "function" //true
所以我们像前面call,apply那样的判断也限制不了 同时
Function.prototype.prototype // undefined
当然。。。没有!
因为 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;
};
总的来说call,apply,bind这三个方法涉及到了js的诸多方法,如果能够完全理解的话,对于学习js会有很大帮助嗷~
如果有错误,请在评论区中指出,非常感谢!
顺便打个广告 leelei的个人博客
最后祝大家拿到心仪的offer
在处理类数组中,发现了两种将数组方法应用于类数组的方法,现将call/apply的常用方式总结一下。当做函数调用;作为对象的方法,给第三方使用;作为原型的方法,给第三方使用
面试题:如何用apply实现一个bind?bind函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现。 bind方法与call/apply最大的区别就是bind返回一个绑定上下文的函数
首先看call和apply,第一个参数就是改变的this指向,写谁就是谁,如果是非严格模式下,传递null或undefined指向的也是window,二者唯一的区别是执行函数时,传递的参数方式不同,call是一个个的传递
call 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数,上述例子中,当foo函数单独调用时内部this绑定为全局对象window。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!