大家好!!!注冊一年多的第一篇博客。
自我介紹: 本人非計算機專業出身,轉行進入前端半年時間,寫的東西可能觀賞性不強,一起進步吧道友們。。。
接下來的一段時間, 我都會不定期整理自己理解的js知識點, 歡迎各路道友吐槽。
進入正題...... (針對新手,老司機不要嘲笑我)
首先, bind/call/apply 這改變this指向的三兄弟我們都很熟悉了, 還有其他改變this指向的方法這里就不多說了哈,要不就跑題了。。。。
你需要知道的前提 : 這三貨都是函數原型(Function.prototype)上的方法, 所以只有函數可以調用。
小白 : “原型是啥?” 每一個構造函數(其實就是普通函數,函數名首字母大寫而已)都有一個prototype原型對象,當你引用某個實例對象上的屬性時, 會先查找該對象本身有沒有該屬性,有就用,沒有就會去構造這個實例的構造函數的原型上去找,還沒有的話就會沿着構造函數的原型對象的__proto__屬性往上找......haha, 好像又跑題了, 關於原型鏈和繼承可以自行百度
真的進入正題。。。。。。
它們不是函數上的方法嗎, 我們先定義個函數, 后面來改變這個函數的this指向
function out(age, sex, type) {
console.log(age, sex, this.age, this.sex, type); }
分別輸出傳入的參數age, sex, type 和 this 上的age , sex,
然后定義個對象, 后面讓this指向該對象, 沒錯,此對象描述的就是我
let my = {
age : 16, sex : 'handsome man' };
先試試bind, 先會用然后再來實現, 由於bind可以傳兩次參數,你可以這樣傳
const bindFunction = out.bind(my,50); //this 指向my 不會直接執行 返回一個明確this指向的函數 bindFunction('women','bind'); // 傳參執行
還可以這樣傳,想怎么傳就怎么傳,只要確保.bind后的第一個參數是你要改變this指向的對象, (如果什么都不傳,this指向window)
const bindFunction = out.bind(my); //this 指向my 不會直接執行 返回一個明確this指向的函數 bindFunction('50','women','bind'); // 傳參執行
執行后輸出 :
50 "women" 16 "handsome man" "bind"
總結一下我們要模擬的bind
1. 函數A調用bind返回一個可執行的函數B, 調用bind時可傳參 可不穿參, 不穿參時this指向window
2. 調用B可繼續傳參, 兩次傳參合並
3. new B() 的構造函數依然是A, 而不是B,並且因為new操作符改變this指向的優先級最高,所以如果使用了new操作符,this指向不應該變,就是該是啥是啥(划重點)
上代碼 :
Function.prototype.myBind = function (target) { // bind 是 function 上的 方法 this 指向 target
if (typeof this !== 'function') {
throw new TypeError('not a function') }; const _this = this, // 保存一下this args = Array.prototype.slice.call(arguments, 1),// arguments是類數組,沒有 slice 方法 保存第一次傳入的參數 temp = function () {}; // 定義一個中間函數 const f = function () { return _this.apply(this instanceof temp ? this : (target || window), args.concat([].slice.call(arguments))); // 合並兩次傳的參數 }; temp.prototype = _this.prototype; // 讓temp 的原型 等於 this的原型 f.prototype = new temp(); //f 繼承 temp 聖杯繼承可以了解下 return f; };
先正常執行一下myBind:
const bind1 = this.out.myBind(my, 50);
// const bind2 = new bind1('women','myBind');
const bind3 = bind1('women','myBind');
結果是:
50 "women" 16 "handsome man" "myBind"
OK , 和bind的輸出結果一樣
再使用new操作符:
const bind1 = this.out.myBind(my, 50);
const bind2 = new bind1('women','myBind'); // const bind3 = bind1('women','myBind');
結果是這樣:此時this不再指向my, 而是指向new 出來的一個空對象,所以this上沒有age和sex兩個屬性
50 "women" undefined undefined "myBind"
到此bind 我們已經完美模擬出來了 -------- 是不是有點小激動 ----------細心的道友會發現myBind 里 還借用了call 和apply!!! 這還不行啊
🆗接下來我們繼續實現myCall 和 myApply
思考:
模擬bind是用call和apply來改變this指向的,
那么我們模擬call和apply要用什么來改變this指向呢? 給大家1s 思考......................................................
沒錯, 就是優先級最低的點(.)操作符, 還記得誰調用this就指向誰嗎, 就是A.eat(),eat中的this就指向A,
基於此我們來實現myCall:
Function.prototype.myCall = function (target) {
target.fn = this; // 給target加一個屬性fn = this
const args = [];
for (let i = 1; i < arguments.length; i ++ ) {
args.push(arguments[i]); // 遍歷把參數push進數組
}
const result = target.fn(...args); // ...是把數組的中括號去掉, 這樣傳進去的就是參數列表了
delete target.fn; // 最后把fn屬性刪掉
return result;
};
這樣就把this指向target了
同理myBind;
Function.prototype.myApply = function (target) {
target.fn = this;// 給target加一個屬性fn = this
var result;
if(arguments[1]) {
result = target.fn(...arguments[1]);// ...是把數組的中括號去掉, 這樣傳進去的就是參數列表了
} else {
result = target.fn();
}
delete target.fn;// 最后把fn屬性刪掉
return result;
};
執行結果:
這樣理論上來說Call和Apply就已經模擬出來了, 但是我還留下了一個坑....................................................
有老司機可以看出來嗎? 一秒鍾時間搶答 -----------------------------------------------------------
沒錯, 我在myCall和myApply 里用了ES6的語法 : ...操作符扒括號(let 和 const 請忽略,用var也可以), 那我們可以完全用es5 來解決 如何把數組里的參數變成參數列表傳進去嗎?
道友們 有思路嗎 ? 給大家個提示: 可以用ES3.0 的 eval() 配合 join() 方法實現 , eval是魔鬼大家請慎用
好記性不如爛筆頭,大家回去自己動手試試吧 !
再演示一遍完整demo