幾種常見的手寫源碼實現
- 2020 年 2 月 2 日
- 筆記
閱讀源碼的好處,不用說都知道,首先進大廠必備,還可以提升自己的能力,學習前人的經驗。源碼往往是前人留下的最佳實踐,我們跟著前人的腳步去學習會讓我們事半功倍。
- call、aplly、bind 實現
- new 實現
- class 實現繼承
- async/await 實現
- reduce 實現
- 實現一個雙向數據綁定
- instanceof 實現
- Array.isArray 實現
- Object.create 的基本實現原理
- getOwnPropertyNames 實現
- promise 實現
- 手寫一個防抖/節流函數
- 柯里化函數的實現
- 手寫一個深拷貝
call、aplly、bind 實現
call、aplly、bind
本質都是改變this
的指向,不同點call、aplly
是直接調用函數,bind
是返回一個新的函數。call
跟aplly
就只有參數上不同。
bind 實現
- 箭頭函數的
this
永遠指向它所在的作用域 - 函數作為構造函數用
new
關鍵字調用時,不應該改變其this
指向,因為new綁定
的優先順序高於顯示綁定
和硬綁定
Function.prototype.mybind = function(thisArg) { if (typeofthis !== 'function') { throwTypeError("Bind must be called on a function"); } // 拿到參數,為了傳給調用者 const args = Array.prototype.slice.call(arguments, 1), // 保存 this self = this, // 構建一個乾淨的函數,用於保存原函數的原型 nop = function() {}, // 綁定的函數 bound = function() { // this instanceof nop, 判斷是否使用 new 來調用 bound // 如果是 new 來調用的話,this的指向就是其實例, // 如果不是 new 調用的話,就改變 this 指向到指定的對象 o return self.apply( thisinstanceof nop ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)) ); }; // 箭頭函數沒有 prototype,箭頭函數this永遠指向它所在的作用域 if (this.prototype) { nop.prototype = this.prototype; } // 修改綁定函數的原型指向 bound.prototype = new nop(); return bound; } }
測試 mybind
const bar = function() { console.log(this.name, arguments); }; bar.prototype.name = 'bar'; const foo = { name: 'foo' }; const bound = bar.mybind(foo, 22, 33, 44); new bound(); // bar, [22, 33, 44] bound(); // foo, [22, 33, 44]
call 實現
bind
是封裝了call
的方法改變了this
的指向並返回一個新的函數,那麼call
是如何做到改變this
的指向呢?原理很簡單,在方法調用模式下,this
總是指向調用它所在方法的對象,this
的指向與所在方法的調用位置有關,而與方法的聲明位置無關(箭頭函數特殊)。先寫一個小demo
來理解一下下。
const foo = { name: 'foo' }; foo.fn = function() { // 這裡的 this 指向了 foo // 因為 foo 調用了 fn, // fn 的 this 就指向了調用它所在方法的對象 foo 上 console.log(this.name); // foo };
利用 this
的機制來實現 call
Function.prototype.mycall = function(thisArg) { // this指向調用call的對象 if (typeofthis !== 'function') { // 調用call的若不是函數則報錯 thrownewTypeError('Error'); } const args = [...arguments].slice(1); thisArg = thisArg || window; // 將調用call函數的對象添加到thisArg的屬性中 thisArg.fn = this; // 執行該屬性 const result = thisArg.fn(...arg); // 刪除該屬性 delete thisArg.fn; // 返回函數執行結果 return result; };
aplly 實現
Function.prototype.myapply = function(thisArg) { if (typeofthis !== 'function') { throwthis + ' is not a function'; } const args = arguments[1]; thisArg.fn = this; const result = thisArg.fn(...arg); delete thisArg.fn; return result; };
測試 mycall myaplly
const bar = function() { console.log(this.name, arguments); }; bar.prototype.name = 'bar'; const foo = { name: 'foo' }; bar.mycall(foo, 1, 2, 3); // foo [1, 2, 3] bar.myaplly(foo, [1, 2, 3]); // foo [1, 2, 3]
reduce 實現原理
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
Array.prototype.myreduce = function reduce(callbackfn) { // 拿到數組 const O = this, len = O.length; // 下標值 let k = 0, // 累加器 accumulator = undefined, // k下標對應的值是否存在 kPresent = false, // 初始值 initialValue = arguments.length > 1 ? arguments[1] : undefined; if (typeof callbackfn !== 'function') { thrownewTypeError(callbackfn + ' is not a function'); } // 數組為空,並且有初始值,報錯 if (len === 0 && arguments.length < 2) { thrownewTypeError('Reduce of empty array with no initial value'); } // 如果初始值存在 if (arguments.length > 1) { // 設置累加器為初始值 accumulator = initialValue; // 初始值不存在 } else { accumulator = O[k]; ++k; } while (k < len) { // 判斷是否為 empty [,,,] kPresent = O.hasOwnProperty(k); if (kPresent) { const kValue = O[k]; // 調用 callbackfn accumulator = callbackfn.apply(undefined, [accumulator, kValue, k, O]); } ++k; } return accumulator; };
測試
const rReduce = ['1', null, undefined, , 3, 4].reduce((a, b) => a + b, 3); const mReduce = ['1', null, undefined, , 3, 4].myreduce((a, b) => a + b, 3); console.log(rReduce, mReduce); // 31nullundefined34 31nullundefined34
new 實現
我們需要知道當
new
的時候做了什麼事情
- 創建一個新對象;
- 將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象)
- 執行構造函數中的程式碼(為這個新對象添加屬性)
- 返回新對象。
因為 new 沒辦法重寫,我們使用 myNew
函數來模擬 new
function myNew() { // 創建一個實例對象 var obj = new Object(); // 取得外部傳入的構造器 var Constructor = Array.prototype.shift.call(arguments); // 實現繼承,實例可以訪問構造器的屬性 obj.__proto__ = Constructor.prototype; // 調用構造器,並改變其 this 指向到實例 var ret = Constructor.apply(obj, arguments); // 如果構造函數返回值是對象則返回這個對象,如果不是對象則返回新的實例對象 returntypeof ret === 'object' ? ret : obj; }
測試 myNew
// ========= 無返回值 ============= const testNewFun = function(name) { this.name = name; }; const newObj = myNew(testNewFun, 'foo'); console.log(newObj); // { name: "foo" } console.log(newObj instanceof testNewFun); // true // ========= 有返回值 ============= const testNewFun = function(name) { this.name = name; return {}; }; const newObj = myNew(testNewFun, 'foo'); console.log(newObj); // {} console.log(newObj instanceof testNewFun); // false
class 實現繼承
主要使用
es5
跟es6
對比看下class
繼承的原理
實現繼承 A extends B
使用 es6
語法
class B { constructor(opt) { this.BName = opt.name; } } class A extends B { constructor() { // 向父類傳參 super({ name: 'B' }); // this 必須在 super() 下面使用 console.log(this); } }
使用 es5
語法
使用寄生組合繼承的方式
- 原型鏈繼承,使子類可以調用父類原型上的方法和屬性
- 借用構造函數繼承,可以實現向父類傳參
- 寄生繼承,創造乾淨的沒有構造方法的函數,用來寄生父類的 prototype
// 實現繼承,通過繼承父類 prototype function __extends(child, parent) { // 修改對象原型 Object.setPrototypeOf(child, parent); // 寄生繼承,創建一個乾淨的構造函數,用於繼承父類的 prototype // 這樣做的好處是,修改子類的 prototype 不會影響父類的 prototype function __() { // 修正 constructor 指向子類 this.constructor = child; } // 原型繼承,繼承父類原型屬性,但是無法向父類構造函數傳參 child.prototype = parent === null ? Object.create(parent) : ((__.prototype = parent.prototype), new __()); } var B = (function() { function B(opt) { this.name = opt.name; } return B; })(); var A = (function(_super) { __extends(A, _super); function A() { // 借用繼承,可以實現向父類傳參, 使用 super 可以向父類傳參 return (_super !== null && _super.apply(this, { name: 'B' })) || this; } return A; })(B);
測試 class
const a = new A(); console.log(a.BName, a.constructor); // B ,ƒ A() {}
async/await 實現
原理就是利用
generator
(生成器)分割程式碼片段。然後我們使用一個函數讓其自迭代,每一個yield
用promise
包裹起來。執行下一步的時機由promise
來控制
async/await
是關鍵字,不能重寫它的方法,我們使用函數來模擬
非同步迭代,模擬非同步函數
function _asyncToGenerator(fn) { returnfunction() { var self = this, args = arguments; // 將返回值promise化 returnnewPromise(function(resolve, reject) { // 獲取迭代器實例 var gen = fn.apply(self, args); // 執行下一步 function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value); } // 拋出異常 function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err); } // 第一次觸發 _next(undefined); }); }; }
執行迭代步驟,處理下次迭代結果
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { // 迭代器完成 resolve(value); } else { // -- 這行程式碼就是精髓 -- // 將所有值promise化 // 比如 yield 1 // const a = Promise.resolve(1) a 是一個 promise // const b = Promise.resolve(a) b 是一個 promise // 可以做到統一 promise 輸出 // 當 promise 執行完之後再執行下一步 // 遞歸調用 next 函數,直到 done == true Promise.resolve(value).then(_next, _throw); } }
測試 _asyncToGenerator
const asyncFunc = _asyncToGenerator(function*() { const e = yieldnewPromise(resolve => { setTimeout(() => { resolve('e'); }, 1000); }); const a = yieldPromise.resolve('a'); const d = yield'd'; const b = yieldPromise.resolve('b'); const c = yieldPromise.resolve('c'); return [a, b, c, d, e]; }); asyncFunc().then(res => { console.log(res); // ['a', 'b', 'c', 'd', 'e'] });
實現一個雙向綁定
defineProperty
版本
// 數據 const data = { text: 'default' }; const input = document.getElementById('input'); const span = document.getElementById('span'); // 數據劫持 Object.defineProperty(data, 'text', { // 數據變化 --> 修改視圖 set(newVal) { input.value = newVal; span.innerHTML = newVal; } }); // 視圖更改 --> 數據變化 input.addEventListener('keyup', function(e) { data.text = e.target.value; });
proxy
版本
// 數據 const data = { text: 'default' }; const input = document.getElementById('input'); const span = document.getElementById('span'); // 數據劫持 const handler = { set(target, key, value) { target[key] = value; // 數據變化 --> 修改視圖 input.value = value; span.innerHTML = value; return value; } }; const proxy = newProxy(data); // 視圖更改 --> 數據變化 input.addEventListener('keyup', function(e) { proxy.text = e.target.value; });
Object.create 的基本實現原理
function create(obj) { function F() {} F.prototype = obj; returnnew F(); }
instanceof 實現
原理:
L
的__proto__
是不是等於R.prototype
,不等於再找L.__proto__.__proto__
直到__proto__
為null
// L 表示左表達式,R 表示右表達式 function instance_of(L, R) { var O = R.prototype; L = L.__proto__; while (true) { if (L === null) returnfalse; // 這裡重點:當 O 嚴格等於 L 時,返回 true if (O === L) returntrue; L = L.__proto__; } }
Array.isArray 實現
Array.myIsArray = function(o) { returnObject.prototype.toString.call(Object(o)) === '[object Array]'; }; console.log(Array.myIsArray([])); // true
getOwnPropertyNames 實現
if (typeofObject.getOwnPropertyNames !== 'function') { Object.getOwnPropertyNames = function(o) { if (o !== Object(o)) { throwTypeError('Object.getOwnPropertyNames called on non-object'); } var props = [], p; for (p in o) { if (Object.prototype.hasOwnProperty.call(o, p)) { props.push(p); } } return props; }; }
Promise 實現
實現原理:其實就是一個發布訂閱者模式
- 構造函數接收一個
executor
函數,並會在new Promise()
時立即執行該函數 then
時收集依賴,將回調函數收集到成功/失敗隊列
executor
函數中調用resolve/reject
函數resolve/reject
函數被調用時會通知觸發隊列中的回調
先看一下整體程式碼,有一個大致的概念

完整程式碼
const isFunction = variable =>typeof variable === 'function'; // 定義Promise的三種狀態常量 const PENDING = 'pending'; const FULFILLED = 'fulfilled'; const REJECTED = 'rejected'; class MyPromise { // 構造函數,new 時觸發 constructor(handle: Function) { try { handle(this._resolve, this._reject); } catch (err) { this._reject(err); } } // 狀態 pending fulfilled rejected private _status: string = PENDING; // 儲存 value,用於 then 返回 private _value: string | undefined = undefined; // 失敗隊列,在 then 時注入,resolve 時觸發 private _rejectedQueues: any = []; // 成功隊列,在 then 時注入,resolve 時觸發 private _fulfilledQueues: any = []; // resovle 時執行的函數 private _resolve = val => { const run = () => { if (this._status !== PENDING) return; this._status = FULFILLED; // 依次執行成功隊列中的函數,並清空隊列 const runFulfilled = value => { let cb; while ((cb = this._fulfilledQueues.shift())) { cb(value); } }; // 依次執行失敗隊列中的函數,並清空隊列 const runRejected = error => { let cb; while ((cb = this._rejectedQueues.shift())) { cb(error); } }; /* * 如果resolve的參數為Promise對象, * 則必須等待該Promise對象狀態改變後當前Promsie的狀態才會改變 * 且狀態取決於參數Promsie對象的狀態 */ if (val instanceof MyPromise) { val.then( value => { this._value = value; runFulfilled(value); }, err => { this._value = err; runRejected(err); } ); } else { this._value = val; runFulfilled(val); } }; // 非同步調用 setTimeout(run); }; // reject 時執行的函數 private _reject = err => { if (this._status !== PENDING) return; // 依次執行失敗隊列中的函數,並清空隊列 const run = () => { this._status = REJECTED; this._value = err; let cb; while ((cb = this._rejectedQueues.shift())) { cb(err); } }; // 為了支援同步的Promise,這裡採用非同步調用 setTimeout(run); }; // then 方法 then(onFulfilled?, onRejected?) { const { _value, _status } = this; // 返回一個新的Promise對象 returnnew MyPromise((onFulfilledNext, onRejectedNext) => { // 封裝一個成功時執行的函數 const fulfilled = value => { try { if (!isFunction(onFulfilled)) { onFulfilledNext(value); } else { const res = onFulfilled(value); if (res instanceof MyPromise) { // 如果當前回調函數返回MyPromise對象,必須等待其狀態改變後在執行下一個回調 res.then(onFulfilledNext, onRejectedNext); } else { //否則會將返回結果直接作為參數,傳入下一個then的回調函數,並立即執行下一個then的回調函數 onFulfilledNext(res); } } } catch (err) { // 如果函數執行出錯,新的Promise對象的狀態為失敗 onRejectedNext(err); } }; // 封裝一個失敗時執行的函數 const rejected = error => { try { if (!isFunction(onRejected)) { onRejectedNext(error); } else { const res = onRejected(error); if (res instanceof MyPromise) { // 如果當前回調函數返回MyPromise對象,必須等待其狀態改變後在執行下一個回調 res.then(onFulfilledNext, onRejectedNext); } else { //否則會將返回結果直接作為參數,傳入下一個then的回調函數,並立即執行下一個then的回調函數 onFulfilledNext(res); } } } catch (err) { // 如果函數執行出錯,新的Promise對象的狀態為失敗 onRejectedNext(err); } }; switch (_status) { // 當狀態為pending時,將then方法回調函數加入執行隊列等待執行 case PENDING: this._fulfilledQueues.push(fulfilled); this._rejectedQueues.push(rejected); break; // 當狀態已經改變時,立即執行對應的回調函數 case FULFILLED: fulfilled(_value); break; case REJECTED: rejected(_value); break; } }); } // catch 方法 catch(onRejected) { returnthis.then(undefined, onRejected); } // finally 方法 finally(cb) { returnthis.then( value => MyPromise.resolve(cb()).then(() => value), reason => MyPromise.resolve(cb()).then(() => { throw reason; }) ); } // 靜態 resolve 方法 static resolve(value) { // 如果參數是MyPromise實例,直接返回這個實例 if (value instanceof MyPromise) return value; returnnew MyPromise(resolve => resolve(value)); } // 靜態 reject 方法 static reject(value) { returnnew MyPromise((resolve, reject) => reject(value)); } // 靜態 all 方法 static all(list) { returnnew MyPromise((resolve, reject) => { // 返回值的集合 let values = []; let count = 0; for (let [i, p] of list.entries()) { // 數組參數如果不是MyPromise實例,先調用MyPromise.resolve this.resolve(p).then( res => { values[i] = res; count++; // 所有狀態都變成fulfilled時返回的MyPromise狀態就變成fulfilled if (count === list.length) resolve(values); }, err => { // 有一個被rejected時返回的MyPromise狀態就變成rejected reject(err); } ); } }); } // 添加靜態race方法 static race(list) { returnnew MyPromise((resolve, reject) => { for (let p of list) { // 只要有一個實例率先改變狀態,新的MyPromise的狀態就跟著改變 this.resolve(p).then( res => { resolve(res); }, err => { reject(err); } ); } }); } }
防抖/截流
防抖函數
onscroll 結束時觸發一次,延遲執行
function debounce(func, wait) { let timeout; returnfunction() { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { func.apply(context, args); }, wait); }; } // 使用 window.onscroll = debounce(function() { console.log('debounce'); }, 1000);
節流函數
onscroll 時,每隔一段時間觸發一次,像水滴一樣
function throttle(fn, delay) { var prevTime = Date.now(); returnfunction() { var curTime = Date.now(); if (curTime - prevTime > delay) { fn.apply(this, arguments); prevTime = curTime; } }; } // 使用 var throtteScroll = throttle(function() { console.log('throtte'); }, 1000); window.onscroll = throtteScroll;
函數柯里化實現
其實我們無時無刻不在使用柯里化函數,只是沒有將它總結出來而已。它的本質就是將一個參數很多的函數分解成單一參數的多個函數。
實際應用中:
- 延遲計算 (用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,開始執行函數)
- 動態創建函數 (參數不夠時會返回接受剩下參數的函數)
- 參數復用(每個參數可以多次復用)
const curry = fn => (judge = (...args) => args.length === fn.length ? fn(...args) : (...arg) => judge(...args, ...arg)); const sum = (a, b, c, d) => a + b + c + d; const currySum = curry(sum); currySum(1)(2)(3)(4); // 10 currySum(1, 2)(3)(4); // 10 currySum(1)(2, 3)(4); // 10
手寫一個深拷貝
淺拷貝只複製地址值,實際上還是指向同一堆記憶體中的數據,深拷貝則是重新創建了一個相同的數據,二者指向的堆記憶體的地址值是不同的。這個時候修改賦值前的變數數據不會影響賦值後的變數。
要實現一個完美的神拷貝太複雜了,這裡簡單介紹一下吧,可以應用於大部分場景了
判斷類型函數
function getType(obj) { const str = Object.prototype.toString.call(obj); const map = { '[object Boolean]': 'boolean', '[object Number]': 'number', '[object String]': 'string', '[object Function]': 'function', '[object Array]': 'array', '[object Date]': 'date', '[object RegExp]': 'regExp', '[object Undefined]': 'undefined', '[object Null]': 'null', '[object Object]': 'object' }; if (obj instanceof Element) { // 判斷是否是dom元素,如div等 return'element'; } return map[str]; }
簡單版深拷貝,列舉三個例子 array
object
function
,可以自行擴展。主要是引發大家的思考
function deepCopy(ori) { const type = getType(ori); let copy; switch (type) { case'array': return copyArray(ori, type, copy); case'object': return copyObject(ori, type, copy); case'function': return copyFunction(ori, type, copy); default: return ori; } } function copyArray(ori, type, copy = []) { for (const [index, value] of ori.entries()) { copy[index] = deepCopy(value); } return copy; } function copyObject(ori, type, copy = {}) { for (const [key, value] ofObject.entries(ori)) { copy[key] = deepCopy(value); } return copy; } function copyFunction(ori, type, copy = () => {}) { const fun = eval(ori.toString()); fun.prototype = ori.prototype return fun }
參考文章
https://cloud.tencent.com/developer/article/1431398 https://www.jianshu.com/p/b4f0425b22a1 https://blog.csdn.net/LL18781132750/article/details/79700089