死磕 36 個 JS 手寫題
作爲一個普通前端,我是真的寫不出 Promise A+ 規範,但是沒關係,我們可以站在巨人的肩膀上,要相信我們現在要走的路,前人都走過,所以可以找找現在社區已經存在的那些優秀的文章,比如工業聚大佬寫的 100 行代碼實現 Promises/A+ 規範,找到這些文章後不是收藏夾喫灰,得找個時間踏踏實實的學,一行一行的磨,直到搞懂爲止。我現在就是這麼幹的。
能收穫什麼
這篇文章總體上分爲 2 類手寫題,前半部分可以歸納爲是常見需求,後半部分則是對現有技術的實現;
-
對常用的需求進行手寫實現,比如數據類型判斷函數、深拷貝等可以直接用於往後的項目中,提高了項目開發效率;
-
對現有關鍵字和 API 的實現,可能需要用到別的知識或 API,比如在寫 forEach 的時候用到了無符號位右移的操作,平時都不怎麼能夠接觸到這玩意,現在遇到了就可以順手把它掌握了。所以手寫這些實現能夠潛移默化的擴展並鞏固自己的 JS 基礎;
-
通過寫各種測試用例,你會知道各種 API 的邊界情況,比如 Promise.all, 你得考慮到傳入參數的各種情況,從而加深了對它們的理解及使用;
閱讀的時候需要做什麼
閱讀的時候,你需要把每行代碼都看懂,知道它在幹什麼,爲什麼要這麼寫,能寫得更好嘛?比如在寫圖片懶加載的時候,一般我們都是根據當前元素的位置和視口進行判斷是否要加載這張圖片,普通程序員寫到這就差不多完成了。而大佬程序員則是會多考慮一些細節的東西,比如性能如何更優?代碼如何更精簡?比如 yeyan1996 寫的圖片懶加載就多考慮了 2 點:比如圖片全部加載完成的時候得把事件監聽給移除;比如加載完一張圖片的時候,得把當前 img 從 imgList 裏移除,起到優化內存的作用。
除了讀通代碼之外,還可以打開 Chrome 的 Script snippet 去寫測試用例跑跑代碼,做到更好的理解以及使用。
在看了幾篇以及寫了很多測試用例的前提下,嘗試自己手寫實現,看看自己到底掌握了多少。條條大路通羅馬,你還能有別的方式實現嘛?或者你能寫得比別人更好嘛?
好了,還楞着幹啥,開始幹活。
數據類型判斷
typeof 可以正確識別:Undefined、Boolean、Number、String、Symbol、Function 等類型的數據,但是對於其他的都會認爲是 object,比如 Null、Date 等,所以通過 typeof 來判斷數據類型會不準確。但是可以使用 Object.prototype.toString 實現。
1function typeOf(obj) {
2 let res = Object.prototype.toString.call(obj).split(' ')[1]
3 res = res.substring(0, res.length - 1).toLowerCase()
4 return res
5}
6typeOf([]) // 'array'
7typeOf({}) // 'object'
8typeOf(new Date) // 'date'
9
10
繼承
原型鏈繼承
1function Animal() {
2 this.colors = ['black', 'white']
3}
4Animal.prototype.getColor = function() {
5 return this.colors
6}
7function Dog() {}
8Dog.prototype = new Animal()
9
10let dog1 = new Dog()
11dog1.colors.push('brown')
12let dog2 = new Dog()
13console.log(dog2.colors) // ['black', 'white', 'brown']
14
15
原型鏈繼承存在的問題:
-
問題 1:原型中包含的引用類型屬性將被所有實例共享;
-
問題 2:子類在實例化的時候不能給父類構造函數傳參;
借用構造函數實現繼承
1function Animal(name) {
2 this.name = name
3 this.getName = function() {
4 return this.name
5 }
6}
7function Dog(name) {
8 Animal.call(this, name)
9}
10Dog.prototype = new Animal()
11
12
借用構造函數實現繼承解決了原型鏈繼承的 2 個問題:引用類型共享問題以及傳參問題。但是由於方法必須定義在構造函數中,所以會導致每次創建子類實例都會創建一遍方法。
組合繼承
組合繼承結合了原型鏈和盜用構造函數,將兩者的優點集中了起來。基本的思路是使用原型鏈繼承原型上的屬性和方法,而通過盜用構造函數繼承實例屬性。這樣既可以把方法定義在原型上以實現重用,又可以讓每個實例都有自己的屬性。
1function Animal(name) {
2 this.name = name
3 this.colors = ['black', 'white']
4}
5Animal.prototype.getName = function() {
6 return this.name
7}
8function Dog(name, age) {
9 Animal.call(this, name)
10 this.age = age
11}
12Dog.prototype = new Animal()
13Dog.prototype.constructor = Dog
14
15let dog1 = new Dog('奶昔', 2)
16dog1.colors.push('brown')
17let dog2 = new Dog('哈赤', 1)
18console.log(dog2)
19// { name: "哈赤", colors: ["black", "white"], age: 1 }
20
21
寄生式組合繼承
組合繼承已經相對完善了,但還是存在問題,它的問題就是調用了 2 次父類構造函數,第一次是在 new Animal(),第二次是在 Animal.call() 這裏。
所以解決方案就是不直接調用父類構造函數給子類原型賦值,而是通過創建空函數 F 獲取父類原型的副本。
寄生式組合繼承寫法上和組合繼承基本類似,區別是如下這裏:
1- Dog.prototype = new Animal()
2- Dog.prototype.constructor = Dog
3
4+ function F() {}
5+ F.prototype = Animal.prototype
6+ let f = new F()
7+ f.constructor = Dog
8+ Dog.prototype = f
9
10
稍微封裝下上面添加的代碼後:
1function object(o) {
2 function F() {}
3 F.prototype = o
4 return new F()
5}
6function inheritPrototype(child, parent) {
7 let prototype = object(parent.prototype)
8 prototype.constructor = child
9 child.prototype = prototype
10}
11inheritPrototype(Dog, Animal)
12
13
如果你嫌棄上面的代碼太多了,還可以基於組合繼承的代碼改成最簡單的寄生式組合繼承:
1- Dog.prototype = new Animal()
2- Dog.prototype.constructor = Dog
3
4+ Dog.prototype = Object.create(Animal.prototype)
5+ Dog.prototype.constructor = Dog
6
7
class 實現繼承
1class Animal {
2 constructor(name) {
3 this.name = name
4 }
5 getName() {
6 return this.name
7 }
8}
9class Dog extends Animal {
10 constructor(name, age) {
11 super(name)
12 this.age = age
13 }
14}
15
16
數組去重
ES5 實現:
1function unique(arr) {
2 var res = arr.filter(function(item, index, array) {
3 return array.indexOf(item) === index
4 })
5 return res
6}
7
8
ES6 實現:
1var unique = arr => [...new Set(arr)]
2
3
數組扁平化
數組扁平化就是將 [1, [2, [3]]] 這種多層的數組拍平成一層 [1, 2, 3]。使用 Array.prototype.flat 可以直接將多層數組拍平成一層:
1[1, [2, [3]]].flat(2) // [1, 2, 3]
2
3
現在就是要實現 flat 這種效果。
ES5 實現:遞歸。
1function flatten(arr) {
2 var result = [];
3 for (var i = 0, len = arr.length; i < len; i++) {
4 if (Array.isArray(arr[i])) {
5 result = result.concat(flatten(arr[i]))
6 } else {
7 result.push(arr[i])
8 }
9 }
10 return result;
11}
12
13
ES6 實現:
1function flatten(arr) {
2 while (arr.some(item => Array.isArray(item))) {
3 arr = [].concat(...arr);
4 }
5 return arr;
6}
7
8
深淺拷貝
淺拷貝:只考慮對象類型。
1function shallowCopy(obj) {
2 if (typeof obj !== 'object') return
3
4 let newObj = obj instanceof Array ? [] : {}
5 for (let key in obj) {
6 if (obj.hasOwnProperty(key)) {
7 newObj[key] = obj[key]
8 }
9 }
10 return newObj
11}
12
13
簡單版深拷貝:只考慮普通對象屬性,不考慮內置對象和函數。
1function deepClone(obj) {
2 if (typeof obj !== 'object') return;
3 var newObj = obj instanceof Array ? [] : {};
4 for (var key in obj) {
5 if (obj.hasOwnProperty(key)) {
6 newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
7 }
8 }
9 return newObj;
10}
11
12
複雜版深克隆:基於簡單版的基礎上,還考慮了內置對象比如 Date、RegExp 等對象和函數以及解決了循環引用的問題。
1const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
2
3function deepClone(target, map = new WeakMap()) {
4 if (map.get(target)) {
5 return target;
6 }
7 // 獲取當前值的構造函數:獲取它的類型
8 let constructor = target.constructor;
9 // 檢測當前對象target是否與正則、日期格式對象匹配
10 if (/^(RegExp|Date)$/i.test(constructor.name)) {
11 // 創建一個新的特殊對象(正則類/日期類)的實例
12 return new constructor(target);
13 }
14 if (isObject(target)) {
15 map.set(target, true); // 爲循環引用的對象做標記
16 const cloneTarget = Array.isArray(target) ? [] : {};
17 for (let prop in target) {
18 if (target.hasOwnProperty(prop)) {
19 cloneTarget[prop] = deepClone(target[prop], map);
20 }
21 }
22 return cloneTarget;
23 } else {
24 return target;
25 }
26}
27
28
事件總線(發佈訂閱模式)
1class EventEmitter {
2 constructor() {
3 this.cache = {}
4 }
5 on(name, fn) {
6 if (this.cache[name]) {
7 this.cache[name].push(fn)
8 } else {
9 this.cache[name] = [fn]
10 }
11 }
12 off(name, fn) {
13 let tasks = this.cache[name]
14 if (tasks) {
15 const index = tasks.findIndex(f => f === fn || f.callback === fn)
16 if (index >= 0) {
17 tasks.splice(index, 1)
18 }
19 }
20 }
21 emit(name, once = false, ...args) {
22 if (this.cache[name]) {
23 // 創建副本,如果回調函數內繼續註冊相同事件,會造成死循環
24 let tasks = this.cache[name].slice()
25 for (let fn of tasks) {
26 fn(...args)
27 }
28 if (once) {
29 delete this.cache[name]
30 }
31 }
32 }
33}
34
35// 測試
36let eventBus = new EventEmitter()
37let fn1 = function(name, age) {
38 console.log(`${name} ${age}`)
39}
40let fn2 = function(name, age) {
41 console.log(`hello, ${name} ${age}`)
42}
43eventBus.on('aaa', fn1)
44eventBus.on('aaa', fn2)
45eventBus.emit('aaa', false, '布蘭', 12)
46// '布蘭 12'
47// 'hello, 布蘭 12'
48
49
解析 URL 參數爲對象
1function parseParam(url) {
2 const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字符串取出來
3 const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割後存到數組中
4 let paramsObj = {};
5 // 將 params 存到對象中
6 paramsArr.forEach(param => {
7 if (/=/.test(param)) { // 處理有 value 的參數
8 let [key, val] = param.split('='); // 分割 key 和 value
9 val = decodeURIComponent(val); // 解碼
10 val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉爲數字
11
12 if (paramsObj.hasOwnProperty(key)) { // 如果對象有 key,則添加一個值
13 paramsObj[key] = [].concat(paramsObj[key], val);
14 } else { // 如果對象沒有這個 key,創建 key 並設置值
15 paramsObj[key] = val;
16 }
17 } else { // 處理沒有 value 的參數
18 paramsObj[param] = true;
19 }
20 })
21
22 return paramsObj;
23}
24
25
字符串模板
1function render(template, data) {
2 const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
3 if (reg.test(template)) { // 判斷模板裏是否有模板字符串
4 const name = reg.exec(template)[1]; // 查找當前模板裏第一個模板字符串的字段
5 template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
6 return render(template, data); // 遞歸的渲染並返回渲染後的結構
7 }
8 return template; // 如果模板沒有模板字符串直接返回
9}
10
11
測試:
1let template = '我是{{name}},年齡{{age}},性別{{sex}}';
2let person = {
3 name: '布蘭',
4 age: 12
5}
6render(template, person); // 我是布蘭,年齡12,性別undefined
7
8
圖片懶加載
與普通的圖片懶加載不同,如下這個多做了 2 個精心處理:
-
圖片全部加載完成後移除事件監聽;
-
加載完的圖片,從 imgList 移除;
1let imgList = [...document.querySelectorAll('img')]
2let length = imgList.length
3
4const imgLazyLoad = function() {
5 let count = 0
6 return function() {
7 let deleteIndexList = []
8 imgList.forEach((img, index) => {
9 let rect = img.getBoundingClientRect()
10 if (rect.top < window.innerHeight) {
11 img.src = img.dataset.src
12 deleteIndexList.push(index)
13 count++
14 if (count === length) {
15 document.removeEventListener('scroll', imgLazyLoad)
16 }
17 }
18 })
19 imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
20 }
21}
22
23// 這裏最好加上防抖處理
24document.addEventListener('scroll', imgLazyLoad)
25
26
參考:圖片懶加載 [1]
函數防抖
觸發高頻事件 N 秒後只會執行一次,如果 N 秒內事件再次觸發,則會重新計時。
簡單版:函數內部支持使用 this 和 event 對象;
1function debounce(func, wait) {
2 var timeout;
3 return function () {
4 var context = this;
5 var args = arguments;
6 clearTimeout(timeout)
7 timeout = setTimeout(function(){
8 func.apply(context, args)
9 }, wait);
10 }
11}
12
13
使用:
1var node = document.getElementById('layout')
2function getUserAction(e) {
3 console.log(this, e) // 分別打印:node 這個節點 和 MouseEvent
4 node.innerHTML = count++;
5};
6node.onmousemove = debounce(getUserAction, 1000)
7
8
最終版:除了支持 this 和 event 外,還支持以下功能:
-
支持立即執行;
-
函數可能有返回值;
-
支持取消功能;
1function debounce(func, wait, immediate) {
2 var timeout, result;
3
4 var debounced = function () {
5 var context = this;
6 var args = arguments;
7
8 if (timeout) clearTimeout(timeout);
9 if (immediate) {
10 // 如果已經執行過,不再執行
11 var callNow = !timeout;
12 timeout = setTimeout(function(){
13 timeout = null;
14 }, wait)
15 if (callNow) result = func.apply(context, args)
16 } else {
17 timeout = setTimeout(function(){
18 func.apply(context, args)
19 }, wait);
20 }
21 return result;
22 };
23
24 debounced.cancel = function() {
25 clearTimeout(timeout);
26 timeout = null;
27 };
28
29 return debounced;
30}
31
32
使用:
1var setUseAction = debounce(getUserAction, 10000, true);
2// 使用防抖
3node.onmousemove = setUseAction
4
5// 取消防抖
6setUseAction.cancel()
7
8
參考:JavaScript 專題之跟着 underscore 學防抖
函數節流
觸發高頻事件,且 N 秒內只執行一次。
簡單版:使用時間戳來實現,立即執行一次,然後每 N 秒執行一次。
1function throttle(func, wait) {
2 var context, args;
3 var previous = 0;
4
5 return function() {
6 var now = +new Date();
7 context = this;
8 args = arguments;
9 if (now - previous > wait) {
10 func.apply(context, args);
11 previous = now;
12 }
13 }
14}
15
16
最終版:支持取消節流;另外通過傳入第三個參數,options.leading 來表示是否可以立即執行一次,opitons.trailing 表示結束調用的時候是否還要執行一次,默認都是 true。注意設置的時候不能同時將 leading 或 trailing 設置爲 false。
1function throttle(func, wait, options) {
2 var timeout, context, args, result;
3 var previous = 0;
4 if (!options) options = {};
5
6 var later = function() {
7 previous = options.leading === false ? 0 : new Date().getTime();
8 timeout = null;
9 func.apply(context, args);
10 if (!timeout) context = args = null;
11 };
12
13 var throttled = function() {
14 var now = new Date().getTime();
15 if (!previous && options.leading === false) previous = now;
16 var remaining = wait - (now - previous);
17 context = this;
18 args = arguments;
19 if (remaining <= 0 || remaining > wait) {
20 if (timeout) {
21 clearTimeout(timeout);
22 timeout = null;
23 }
24 previous = now;
25 func.apply(context, args);
26 if (!timeout) context = args = null;
27 } else if (!timeout && options.trailing !== false) {
28 timeout = setTimeout(later, remaining);
29 }
30 };
31
32 throttled.cancel = function() {
33 clearTimeout(timeout);
34 previous = 0;
35 timeout = null;
36 }
37 return throttled;
38}
39
40
節流的使用就不拿代碼舉例了,參考防抖的寫就行。
參考:JavaScript 專題之跟着 underscore 學節流
函數柯里化
什麼叫函數柯里化?其實就是將使用多個參數的函數轉換成一系列使用一個參數的函數的技術。還不懂?來舉個例子。
1function add(a, b, c) {
2 return a + b + c
3}
4add(1, 2, 3)
5let addCurry = curry(add)
6addCurry(1)(2)(3)
7
8
現在就是要實現 curry 這個函數,使函數從一次調用傳入多個參數變成多次調用每次傳一個參數。
1function curry(fn) {
2 let judge = (...args) => {
3 if (args.length == fn.length) return fn(...args)
4 return (...arg) => judge(...args, ...arg)
5 }
6 return judge
7}
8
9
偏函數
什麼是偏函數?偏函數就是將一個 n 參的函數轉換成固定 x 參的函數,剩餘參數(n - x)將在下次調用全部傳入。舉個例子:
1function add(a, b, c) {
2 return a + b + c
3}
4let partialAdd = partial(add, 1)
5partialAdd(2, 3)
6
7
發現沒有,其實偏函數和函數柯里化有點像,所以根據函數柯里化的實現,能夠能很快寫出偏函數的實現:
1function partial(fn, ...args) {
2 return (...arg) => {
3 return fn(...args, ...arg)
4 }
5}
6
7
如上這個功能比較簡單,現在我們希望偏函數能和柯里化一樣能實現佔位功能,比如:
1function clg(a, b, c) {
2 console.log(a, b, c)
3}
4let partialClg = partial(clg, '_', 2)
5partialClg(1, 3) // 依次打印:1, 2, 3
6
7
_
佔的位其實就是 1 的位置。相當於:partial(clg, 1, 2),然後 partialClg(3)。明白了原理,我們就來寫實現:
1function partial(fn, ...args) {
2 return (...arg) => {
3 args[index] =
4 return fn(...args, ...arg)
5 }
6}
7
8
JSONP
JSONP 核心原理:script 標籤不受同源策略約束,所以可以用來進行跨域請求,優點是兼容性好,但是隻能用於 GET 請求;
1const jsonp = ({ url, params, callbackName }) => {
2 const generateUrl = () => {
3 let dataSrc = ''
4 for (let key in params) {
5 if (params.hasOwnProperty(key)) {
6 dataSrc += `${key}=${params[key]}&`
7 }
8 }
9 dataSrc += `callback=${callbackName}`
10 return `${url}?${dataSrc}`
11 }
12 return new Promise((resolve, reject) => {
13 const scriptEle = document.createElement('script')
14 scriptEle.src = generateUrl()
15 document.body.appendChild(scriptEle)
16 window[callbackName] = data => {
17 resolve(data)
18 document.removeChild(scriptEle)
19 }
20 })
21}
22
23
AJAX
1const getJSON = function(url) {
2 return new Promise((resolve, reject) => {
3 const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
4 xhr.open('GET', url, false);
5 xhr.setRequestHeader('Accept', 'application/json');
6 xhr.onreadystatechange = function() {
7 if (xhr.readyState !== 4) return;
8 if (xhr.status === 200 || xhr.status === 304) {
9 resolve(xhr.responseText);
10 } else {
11 reject(new Error(xhr.responseText));
12 }
13 }
14 xhr.send();
15 })
16}
17
18
實現數組原型方法
forEach
1Array.prototype.forEach2 = function(callback, thisArg) {
2 if (this == null) {
3 throw new TypeError('this is null or not defined')
4 }
5 if (typeof callback !== "function") {
6 throw new TypeError(callback + ' is not a function')
7 }
8 const O = Object(this) // this 就是當前的數組
9 const len = O.length >>> 0 // 後面有解釋
10 let k = 0
11 while (k < len) {
12 if (k in O) {
13 callback.call(thisArg, O[k], k, O);
14 }
15 k++;
16 }
17}
18
19
參考:forEach#polyfill[2]
O.length >>> 0 是什麼操作?就是無符號右移 0 位,那有什麼意義嘛?就是爲了保證轉換後的值爲正整數。其實底層做了 2 層轉換,第一是非 number 轉成 number 類型,第二是將 number 轉成 Uint32 類型。感興趣可以閱讀 something >>> 0 是什麼意思?[3]。
map
基於 forEach 的實現能夠很容易寫出 map 的實現:
1- Array.prototype.forEach2 = function(callback, thisArg) {
2+ Array.prototype.map2 = function(callback, thisArg) {
3 if (this == null) {
4 throw new TypeError('this is null or not defined')
5 }
6 if (typeof callback !== "function") {
7 throw new TypeError(callback + ' is not a function')
8 }
9 const O = Object(this)
10 const len = O.length >>> 0
11- let k = 0
12+ let k = 0, res = []
13 while (k < len) {
14 if (k in O) {
15- callback.call(thisArg, O[k], k, O);
16+ res[k] = callback.call(thisArg, O[k], k, O);
17 }
18 k++;
19 }
20+ return res
21}
22
23
filter
同樣,基於 forEach 的實現能夠很容易寫出 filter 的實現:
1- Array.prototype.forEach2 = function(callback, thisArg) {
2+ Array.prototype.filter2 = function(callback, thisArg) {
3 if (this == null) {
4 throw new TypeError('this is null or not defined')
5 }
6 if (typeof callback !== "function") {
7 throw new TypeError(callback + ' is not a function')
8 }
9 const O = Object(this)
10 const len = O.length >>> 0
11- let k = 0
12+ let k = 0, res = []
13 while (k < len) {
14 if (k in O) {
15- callback.call(thisArg, O[k], k, O);
16+ if (callback.call(thisArg, O[k], k, O)) {
17+ res.push(O[k])
18+ }
19 }
20 k++;
21 }
22+ return res
23}
24
25
some
同樣,基於 forEach 的實現能夠很容易寫出 some 的實現:
1- Array.prototype.forEach2 = function(callback, thisArg) {
2+ Array.prototype.some2 = function(callback, thisArg) {
3 if (this == null) {
4 throw new TypeError('this is null or not defined')
5 }
6 if (typeof callback !== "function") {
7 throw new TypeError(callback + ' is not a function')
8 }
9 const O = Object(this)
10 const len = O.length >>> 0
11 let k = 0
12 while (k < len) {
13 if (k in O) {
14- callback.call(thisArg, O[k], k, O);
15+ if (callback.call(thisArg, O[k], k, O)) {
16+ return true
17+ }
18 }
19 k++;
20 }
21+ return false
22}
23
24
reduce
1Array.prototype.reduce2 = function(callback, initialValue) {
2 if (this == null) {
3 throw new TypeError('this is null or not defined')
4 }
5 if (typeof callback !== "function") {
6 throw new TypeError(callback + ' is not a function')
7 }
8 const O = Object(this)
9 const len = O.length >>> 0
10 let k = 0, acc
11
12 if (arguments.length > 1) {
13 acc = initialValue
14 } else {
15 // 沒傳入初始值的時候,取數組中第一個非 empty 的值爲初始值
16 while (k < len && !(k in O)) {
17 k++
18 }
19 if (k > len) {
20 throw new TypeError( 'Reduce of empty array with no initial value' );
21 }
22 acc = O[k++]
23 }
24 while (k < len) {
25 if (k in O) {
26 acc = callback(acc, O[k], k, O)
27 }
28 k++
29 }
30 return acc
31}
32
33
實現函數原型方法
call
使用一個指定的 this 值和一個或多個參數來調用一個函數。
實現要點:
-
this 可能傳入 null;
-
傳入不固定個數的參數;
-
函數可能有返回值;
1Function.prototype.call2 = function (context) {
2 var context = context || window;
3 context.fn = this;
4
5 var args = [];
6 for(var i = 1, len = arguments.length; i < len; i++) {
7 args.push('arguments[' + i + ']');
8 }
9
10 var result = eval('context.fn(' + args +')');
11
12 delete context.fn
13 return result;
14}
15
16
apply
apply 和 call 一樣,唯一的區別就是 call 是傳入不固定個數的參數,而 apply 是傳入一個數組。
實現要點:
-
this 可能傳入 null;
-
傳入一個數組;
-
函數可能有返回值;
1Function.prototype.apply2 = function (context, arr) {
2 var context = context || window;
3 context.fn = this;
4
5 var result;
6 if (!arr) {
7 result = context.fn();
8 } else {
9 var args = [];
10 for (var i = 0, len = arr.length; i < len; i++) {
11 args.push('arr[' + i + ']');
12 }
13 result = eval('context.fn(' + args + ')')
14 }
15
16 delete context.fn
17 return result;
18}
19
20
bind
bind 方法會創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其餘參數將作爲新函數的參數,供調用時使用。
實現要點:
-
bind() 除了 this 外,還可傳入多個參數;
-
bing 創建的新函數可能傳入多個參數;
-
新函數可能被當做構造函數調用;
-
函數可能有返回值;
1Function.prototype.bind2 = function (context) {
2 var self = this;
3 var args = Array.prototype.slice.call(arguments, 1);
4
5 var fNOP = function () {};
6
7 var fBound = function () {
8 var bindArgs = Array.prototype.slice.call(arguments);
9 return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
10 }
11
12 fNOP.prototype = this.prototype;
13 fBound.prototype = new fNOP();
14 return fBound;
15}
16
17
實現 new 關鍵字
new 運算符用來創建用戶自定義的對象類型的實例或者具有構造函數的內置對象的實例。
實現要點:
-
new 會產生一個新對象;
-
新對象需要能夠訪問到構造函數的屬性,所以需要重新指定它的原型;
-
構造函數可能會顯示返回;
1function objectFactory() {
2 var obj = new Object()
3 Constructor = [].shift.call(arguments);
4 obj.__proto__ = Constructor.prototype;
5 var ret = Constructor.apply(obj, arguments);
6
7 // ret || obj 這裏這麼寫考慮了構造函數顯示返回 null 的情況
8 return typeof ret === 'object' ? ret || obj : obj;
9};
10
11
使用:
1function person(name, age) {
2 this.name = name
3 this.age = age
4}
5let p = objectFactory(person, '布蘭', 12)
6console.log(p) // { name: '布蘭', age: 12 }
7
8
實現 instanceof 關鍵字
instanceof 就是判斷構造函數的 prototype 屬性是否出現在實例的原型鏈上。
1function instanceOf(left, right) {
2 let proto = left.__proto__
3 while (true) {
4 if (proto === null) return false
5 if (proto === right.prototype) {
6 return true
7 }
8 proto = proto.__proto__
9 }
10}
11
12
上面的 left.proto 這種寫法可以換成 Object.getPrototypeOf(left)。
實現 Object.create
Object.create() 方法創建一個新對象,使用現有的對象來提供新創建的對象的__proto__。
1Object.create2 = function(proto, propertyObject = undefined) {
2 if (typeof proto !== 'object' && typeof proto !== 'function') {
3 throw new TypeError('Object prototype may only be an Object or null.')
4 if (propertyObject == null) {
5 new TypeError('Cannot convert undefined or null to object')
6 }
7 function F() {}
8 F.prototype = proto
9 const obj = new F()
10 if (propertyObject != undefined) {
11 Object.defineProperties(obj, propertyObject)
12 }
13 if (proto === null) {
14 // 創建一個沒有原型對象的對象,Object.create(null)
15 obj.__proto__ = null
16 }
17 return obj
18}
19
20
實現 Object.assign
1Object.assign2 = function(target, ...source) {
2 if (target == null) {
3 throw new TypeError('Cannot convert undefined or null to object')
4 }
5 let ret = Object(target)
6 source.forEach(function(obj) {
7 if (obj != null) {
8 for (let key in obj) {
9 if (obj.hasOwnProperty(key)) {
10 ret[key] = obj[key]
11 }
12 }
13 }
14 })
15 return ret
16}
17
18
實現 JSON.stringify
JSON.stringify([, replacer [, space]) 方法是將一個 JavaScript 值 (對象或者數組) 轉換爲一個 JSON 字符串。此處模擬實現,不考慮可選的第二個參數 replacer 和第三個參數 space,如果對這兩個參數的作用還不瞭解,建議閱讀 MDN[4] 文檔。
- 基本數據類型:
-
undefined 轉換之後仍是 undefined(類型也是 undefined)
-
boolean 值轉換之後是字符串 "false"/"true"
-
number 類型 (除了 NaN 和 Infinity) 轉換之後是字符串類型的數值
-
symbol 轉換之後是 undefined
-
null 轉換之後是字符串 "null"
-
string 轉換之後仍是 string
-
NaN 和 Infinity 轉換之後是字符串 "null"
-
函數類型:轉換之後是 undefined
-
如果是對象類型 (非函數)
-
如果有 toJSON() 方法,那麼序列化 toJSON() 的返回值。
-
如果屬性值中出現了 undefined、任意的函數以及 symbol 值,忽略。
-
所有以 symbol 爲屬性鍵的屬性都會被完全忽略掉。
-
如果是一個數組:如果屬性值中出現了 undefined、任意的函數以及 symbol,轉換成字符串 "null" ;
-
如果是 RegExp 對象:返回 {} (類型是 string);
-
如果是 Date 對象,返回 Date 的 toJSON 字符串值;
-
如果是普通對象;
- 對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。
1function jsonStringify(data) {
2 let dataType = typeof data;
3
4 if (dataType !== 'object') {
5 let result = data;
6 //data 可能是 string/number/null/undefined/boolean
7 if (Number.isNaN(data) || data === Infinity) {
8 //NaN 和 Infinity 序列化返回 "null"
9 result = "null";
10 } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
11 //function 、undefined 、symbol 序列化返回 undefined
12 return undefined;
13 } else if (dataType === 'string') {
14 result = '"' + data + '"';
15 }
16 //boolean 返回 String()
17 return String(result);
18 } else if (dataType === 'object') {
19 if (data === null) {
20 return "null"
21 } else if (data.toJSON && typeof data.toJSON === 'function') {
22 return jsonStringify(data.toJSON());
23 } else if (data instanceof Array) {
24 let result = [];
25 //如果是數組
26 //toJSON 方法可以存在於原型鏈中
27 data.forEach((item, index) => {
28 if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
29 result[index] = "null";
30 } else {
31 result[index] = jsonStringify(item);
32 }
33 });
34 result = "[" + result + "]";
35 return result.replace(/'/g, '"');
36
37 } else {
38 //普通對象
39 /**
40 * 循環引用拋錯(暫未檢測,循環引用時,堆棧溢出)
41 * symbol key 忽略
42 * undefined、函數、symbol 爲屬性值,被忽略
43 */
44 let result = [];
45 Object.keys(data).forEach((item, index) => {
46 if (typeof item !== 'symbol') {
47 //key 如果是symbol對象,忽略
48 if (data[item] !== undefined && typeof data[item] !== 'function'
49 && typeof data[item] !== 'symbol') {
50 //鍵值如果是 undefined、函數、symbol 爲屬性值,忽略
51 result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
52 }
53 }
54 });
55 return ("{" + result + "}").replace(/'/g, '"');
56 }
57 }
58}
59
60
參考:實現 JSON.stringify[5]
實現 JSON.parse
介紹 2 種方法實現:
-
eval 實現;
-
new Function 實現;
eval 實現
第一種方式最簡單,也最直觀,就是直接調用 eval,代碼如下:
1var json = '{"a":"1", "b":2}';
2var obj = eval("(" + json + ")"); // obj 就是 json 反序列化之後得到的對象
3
4
但是直接調用 eval 會存在安全問題,如果數據中可能不是 json 數據,而是可執行的 JavaScript 代碼,那很可能會造成 XSS 攻擊。因此,在調用 eval 之前,需要對數據進行校驗。
1var rx_one = /^[\],:{}\s]*$/;
2var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
3var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
4var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
5
6if (
7 rx_one.test(
8 json.replace(rx_two, "@")
9 .replace(rx_three, "]")
10 .replace(rx_four, "")
11 )
12) {
13 var obj = eval("(" +json + ")");
14}
15
16
參考:JSON.parse 三種實現方式 [6]
new Function 實現
Function 與 eval 有相同的字符串參數特性。
1var json = '{"name":"小姐姐", "age":20}';
2var obj = (new Function('return ' + json))();
3
4
實現 Promise
實現 Promise 需要完全讀懂 Promise A+ 規範 [7],不過從總體的實現上看,有如下幾個點需要考慮到:
-
then 需要支持鏈式調用,所以得返回一個新的 Promise;
-
處理異步問題,所以得先用 onResolvedCallbacks 和 onRejectedCallbacks 分別把成功和失敗的回調存起來;
-
爲了讓鏈式調用正常進行下去,需要判斷 onFulfilled 和 onRejected 的類型;
-
onFulfilled 和 onRejected 需要被異步調用,這裏用 setTimeout 模擬異步;
-
處理 Promise 的 resolve;
1const PENDING = 'pending';
2const FULFILLED = 'fulfilled';
3const REJECTED = 'rejected';
4
5class Promise {
6 constructor(executor) {
7 this.status = PENDING;
8 this.value = undefined;
9 this.reason = undefined;
10 this.onResolvedCallbacks = [];
11 this.onRejectedCallbacks = [];
12
13 let resolve = (value) = > {
14 if (this.status === PENDING) {
15 this.status = FULFILLED;
16 this.value = value;
17 this.onResolvedCallbacks.forEach((fn) = > fn());
18 }
19 };
20
21 let reject = (reason) = > {
22 if (this.status === PENDING) {
23 this.status = REJECTED;
24 this.reason = reason;
25 this.onRejectedCallbacks.forEach((fn) = > fn());
26 }
27 };
28
29 try {
30 executor(resolve, reject);
31 } catch (error) {
32 reject(error);
33 }
34 }
35
36 then(onFulfilled, onRejected) {
37 // 解決 onFufilled,onRejected 沒有傳值的問題
38 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
39 // 因爲錯誤的值要讓後面訪問到,所以這裏也要拋出錯誤,不然會在之後 then 的 resolve 中捕獲
40 onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
41 throw err;
42 };
43 // 每次調用 then 都返回一個新的 promise
44 let promise2 = new Promise((resolve, reject) = > {
45 if (this.status === FULFILLED) {
46 //Promise/A+ 2.2.4 --- setTimeout
47 setTimeout(() = > {
48 try {
49 let x = onFulfilled(this.value);
50 // x可能是一個proimise
51 resolvePromise(promise2, x, resolve, reject);
52 } catch (e) {
53 reject(e);
54 }
55 }, 0);
56 }
57
58 if (this.status === REJECTED) {
59 //Promise/A+ 2.2.3
60 setTimeout(() = > {
61 try {
62 let x = onRejected(this.reason);
63 resolvePromise(promise2, x, resolve, reject);
64 } catch (e) {
65 reject(e);
66 }
67 }, 0);
68 }
69
70 if (this.status === PENDING) {
71 this.onResolvedCallbacks.push(() = > {
72 setTimeout(() = > {
73 try {
74 let x = onFulfilled(this.value);
75 resolvePromise(promise2, x, resolve, reject);
76 } catch (e) {
77 reject(e);
78 }
79 }, 0);
80 });
81
82 this.onRejectedCallbacks.push(() = > {
83 setTimeout(() = > {
84 try {
85 let x = onRejected(this.reason);
86 resolvePromise(promise2, x, resolve, reject);
87 } catch (e) {
88 reject(e);
89 }
90 }, 0);
91 });
92 }
93 });
94
95 return promise2;
96 }
97}
98const resolvePromise = (promise2, x, resolve, reject) = > {
99 // 自己等待自己完成是錯誤的實現,用一個類型錯誤,結束掉 promise Promise/A+ 2.3.1
100 if (promise2 === x) {
101 return reject(
102 new TypeError("Chaining cycle detected for promise #<Promise>"));
103 }
104 // Promise/A+ 2.3.3.3.3 只能調用一次
105 let called;
106 // 後續的條件要嚴格判斷 保證代碼能和別的庫一起使用
107 if ((typeof x === "object" && x != null) || typeof x === "function") {
108 try {
109 // 爲了判斷 resolve 過的就不用再 reject 了(比如 reject 和 resolve 同時調用的時候) Promise/A+ 2.3.3.1
110 let then = x.then;
111 if (typeof then === "function") {
112 // 不要寫成 x.then,直接 then.call 就可以了 因爲 x.then 會再次取值,Object.defineProperty Promise/A+ 2.3.3.3
113 then.call(
114 x, (y) = > {
115 // 根據 promise 的狀態決定是成功還是失敗
116 if (called) return;
117 called = true;
118 // 遞歸解析的過程(因爲可能 promise 中還有 promise) Promise/A+ 2.3.3.3.1
119 resolvePromise(promise2, y, resolve, reject);
120 }, (r) = > {
121 // 只要失敗就失敗 Promise/A+ 2.3.3.3.2
122 if (called) return;
123 called = true;
124 reject(r);
125 });
126 } else {
127 // 如果 x.then 是個普通值就直接返回 resolve 作爲結果 Promise/A+ 2.3.3.4
128 resolve(x);
129 }
130 } catch (e) {
131 // Promise/A+ 2.3.3.2
132 if (called) return;
133 called = true;
134 reject(e);
135 }
136 } else {
137 // 如果 x 是個普通值就直接返回 resolve 作爲結果 Promise/A+ 2.3.4
138 resolve(x);
139 }
140};
141
142
Promise 寫完之後可以通過 promises-aplus-tests 這個包對我們寫的代碼進行測試,看是否符合 A+ 規範。不過測試前還得加一段代碼:
1// promise.js
2// 這裏是上面寫的 Promise 全部代碼
3Promise.defer = Promise.deferred = function () {
4 let dfd = {}
5 dfd.promise = new Promise((resolve,reject)=>{
6 dfd.resolve = resolve;
7 dfd.reject = reject;
8 });
9 return dfd;
10}
11module.exports = Promise;
12
13
14
全局安裝:
1npm i promises-aplus-tests -g
2
3
終端下執行驗證命令:
1promises-aplus-tests promise.js
2
3
上面寫的代碼可以順利通過全部 872 個測試用例。
參考:
-
BAT 前端經典面試問題:史上最最最詳細的手寫 Promise 教程 [8]
-
100 行代碼實現 Promises/A+ 規範 [9]
Promise.resolve
Promsie.resolve(value) 可以將任何值轉成值爲 value 狀態是 fulfilled 的 Promise,但如果傳入的值本身是 Promise 則會原樣返回它。
1Promise.resolve = function(value) {
2 // 如果是 Promsie,則直接輸出它
3 if(value instanceof Promise){
4 return value
5 }
6 return new Promise(resolve => resolve(value))
7}
8
9
參考:深入理解 Promise[10]
Promise.reject
和 Promise.resolve() 類似,Promise.reject() 會實例化一個 rejected 狀態的 Promise。但與 Promise.resolve() 不同的是,如果給 Promise.reject() 傳遞一個 Promise 對象,則這個對象會成爲新 Promise 的值。
1Promise.reject = function(reason) {
2 return new Promise((resolve, reject) => reject(reason))
3}
4
5
Promise.all
Promise.all 的規則是這樣的:
-
傳入的所有 Promsie 都是 fulfilled,則返回由他們的值組成的,狀態爲 fulfilled 的新 Promise;
-
只要有一個 Promise 是 rejected,則返回 rejected 狀態的新 Promsie,且它的值是第一個 rejected 的 Promise 的值;
-
只要有一個 Promise 是 pending,則返回一個 pending 狀態的新 Promise;
1Promise.all = function(promiseArr) {
2 let index = 0, result = []
3 return new Promise((resolve, reject) => {
4 promiseArr.forEach((p, i) => {
5 Promise.resolve(p).then(val => {
6 index++
7 result[i] = val
8 if (index === promiseArr.length) {
9 resolve(result)
10 }
11 }, err => {
12 reject(err)
13 })
14 })
15 })
16}
17
18
Promise.race
Promise.race 會返回一個由所有可迭代實例中第一個 fulfilled 或 rejected 的實例包裝後的新實例。
1Promise.race = function(promiseArr) {
2 return new Promise((resolve, reject) => {
3 promiseArr.forEach(p => {
4 Promise.resolve(p).then(val => {
5 resolve(val)
6 }, err => {
7 rejecte(err)
8 })
9 })
10 })
11}
12
13
Promise.allSettled
Promise.allSettled 的規則是這樣:
-
所有 Promise 的狀態都變化了,那麼新返回一個狀態是 fulfilled 的 Promise,且它的值是一個數組,數組的每項由所有 Promise 的值和狀態組成的對象;
-
如果有一個是 pending 的 Promise,則返回一個狀態是 pending 的新實例;
1Promise.allSettled = function(promiseArr) {
2 let result = []
3
4 return new Promise((resolve, reject) => {
5 promiseArr.forEach((p, i) => {
6 Promise.resolve(p).then(val => {
7 result.push({
8 status: 'fulfilled',
9 value: val
10 })
11 if (result.length === promiseArr.length) {
12 resolve(result)
13 }
14 }, err => {
15 result.push({
16 status: 'rejected',
17 reason: err
18 })
19 if (result.length === promiseArr.length) {
20 resolve(result)
21 }
22 })
23 })
24 })
25}
26
27
Promise.any
Promise.any 的規則是這樣:
-
空數組或者所有 Promise 都是 rejected,則返回狀態是 rejected 的新 Promsie,且值爲 AggregateError 的錯誤;
-
只要有一個是 fulfilled 狀態的,則返回第一個是 fulfilled 的新實例;
-
其他情況都會返回一個 pending 的新實例;
1Promise.any = function(promiseArr) {
2 let index = 0
3 return new Promise((resolve, reject) => {
4 if (promiseArr.length === 0) return
5 promiseArr.forEach((p, i) => {
6 Promise.resolve(p).then(val => {
7 resolve(val)
8
9 }, err => {
10 index++
11 if (index === promiseArr.length) {
12 reject(new AggregateError('All promises were rejected'))
13 }
14 })
15 })
16 })
17}
18
19
後話
能看到這裏的對代碼都是真愛了,畢竟代碼這玩意看起來是真的很枯燥,但是如果看懂了後,就會像打遊戲贏了一樣開心,而且這玩意會上癮,當你通關了越多的關卡後,你的能力就會拔高一個層次。用標題的話來說就是:搞懂後,提升真的大。加油吧💪,乾飯人
噢不,代碼人。
參考資料
[1]
圖片懶加載: https://juejin.cn/post/6844903856489365518#heading-19
[2]
forEach#polyfill: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#polyfill
[3]
something >>> 0 是什麼意思: https://zhuanlan.zhihu.com/p/100790268
[4]
stringify: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
[5]
實現 JSON.stringify: https://github.com/YvetteLau/Step-By-Step/issues/39#issuecomment-508327280
[6]
JSON.parse 三種實現方式: https://github.com/youngwind/blog/issues/115#issue-300869613
[7]
Promise A+ 規範: https://promisesaplus.com/
[8]
BAT 前端經典面試問題:史上最最最詳細的手寫 Promise 教程: https://juejin.cn/post/6844903625769091079
[9]
100 行代碼實現 Promises/A+ 規範: https://mp.weixin.qq.com/s/qdJ0Xd8zTgtetFdlJL3P1g
[10]
深入理解 Promise: https://bubuzou.com/2020/10/22/promise/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YXEiK8NNx-Bo81371rVCaQ