春天到了,又到了交配,啊 ,不是。。又到了找工做的季節。相信不少朋友都會被問到過這樣的一個JS問題,如何實現call
| apply
| bind
,不少朋友只會用可是不會寫,或者是死記硬背寫法,等到面試官提問的時候,支支吾吾講不清楚,今天我將教會你們徹底理解這個破題!es6
這是一個很方便,可是同時又容易出錯的屬性。面試
咱們只要記住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。
向上面這種簡單的你們都能理解,看看這個容易搞錯的app
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指的也是第一個參數。this
他的特性是把fn中的this指向第一個參數,當咱們使用的時候是這樣的。prototype
它實現了把 sayName中的this指向了 obj,即this.nickName
=>obj.nickName
code
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;
context
的一個屬性result
context
對象,誰樂意乾乾淨淨進來,出去的時候帶了一坨屎啊。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; }
完事了嗎?
固然沒有,做爲男人怎麼能夠那麼快完事兒?
想一想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; };
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。
固然沒有!
還有一個須要注意的點
當咱們執行到 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