閱讀源碼的好處,不用說都知道,首先進大廠必備,還可以提升自己的能力,學習前人的經驗。源碼往往是前人留下的最佳實踐,我們跟著前人的腳步去學習會讓我們事半功倍。
- 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 (typeof this !== ‘function’) {
throw TypeError(“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(
this instanceof 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 (typeof this !== ‘function’) {
// 調用call的若不是函數則報錯
throw new TypeError(‘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 (typeof this !== ‘function’) {
throw this + ‘ 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’) {
throw new TypeError(callbackfn + ‘ is not a function’);
}
// 數組爲空,並且有初始值,報錯
if (len === 0 && arguments.length < 2) {
throw new TypeError(‘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);
// 如果構造函數返回值是對象則返回這個對象,如果不是對象則返回新的實例對象
return typeof 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) {
return function() {
var self = this,
args = arguments;
// 將返回值promise化
return new Promise(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 = yield new Promise(resolve => {
setTimeout(() => {
resolve(‘e’);
}, 1000);
});
const a = yield Promise.resolve(‘a’);
const d = yield ‘d’;
const b = yield Promise.resolve(‘b’);
const c = yield Promise.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 = new Proxy(data);
// 視圖更改 –> 數據變化
input.addEventListener(‘keyup’, function(e) {
proxy.text = e.target.value;
});
Object.create 的基本實現原理
function create(obj) {
function F() {}
F.prototype = obj;
return new 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) return false;
// 這裏重點:當 O 嚴格等于 L 時,返回 true
if (O === L) return true;
L = L.__proto__;
}
}
Array.isArray 實現
Array.myIsArray = function(o) {
return Object.prototype.toString.call(Object(o)) === ‘[object Array]’;
};
console.log(Array.myIsArray([])); // true
getOwnPropertyNames 實現
if (typeof Object.getOwnPropertyNames !== ‘function’) {
Object.getOwnPropertyNames = function(o) {
if (o !== Object(o)) {
throw TypeError(‘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 函數被調用時會通知觸發隊列中的回調
先看一下整體代碼,有一個大致的概念