爲何要寫這類文章前端
做爲一個程序員,代碼能力毋庸置疑是很是很是重要的,就像如今爲何大廠面試基本都問什麼 API 怎麼實現可見其重要性。我想說的是竟然手寫這麼重要,那咱們就必須掌握它,因此文章標題用了死磕,一點也不過度,也但願不被認爲是標題黨。node
做爲一個普通前端,我是真的寫不出 Promise A+ 規範,可是不要緊,咱們能夠站在巨人的肩膀上,要相信咱們如今要走的路,前人都走過,因此能夠找找如今社區已經存在的那些優秀的文章,好比工業聚大佬寫的 100 行代碼實現 Promises/A+ 規範,找到這些文章後不是收藏夾吃灰,得找個時間踏踏實實的學,一行一行的磨,直到搞懂爲止。我如今就是這麼幹的。git
能收穫什麼程序員
這篇文章整體上分爲 2 類手寫題,前半部分能夠概括爲是常見需求,後半部分則是對現有技術的實現;github
閱讀的時候須要作什麼面試
閱讀的時候,你須要把每行代碼都看懂,知道它在幹什麼,爲何要這麼寫,能寫得更好嘛?好比在寫圖片懶加載的時候,通常咱們都是根據當前元素的位置和視口進行判斷是否要加載這張圖片,普通程序員寫到這就差很少完成了。而大佬程序員則是會多考慮一些細節的東西,好比性能如何更優?代碼如何更精簡?好比 yeyan1996 寫的圖片懶加載就多考慮了 2 點:好比圖片所有加載完成的時候得把事件監聽給移除;好比加載完一張圖片的時候,得把當前 img 從 imgList 裏移除,起到優化內存的做用。shell
除了讀通代碼以外,還能夠打開 Chrome 的 Script snippet 去寫測試用例跑跑代碼,作到更好的理解以及使用。npm
在看了幾篇以及寫了不少測試用例的前提下,嘗試本身手寫實現,看看本身到底掌握了多少。條條大路通羅馬,你還能有別的方式實現嘛?或者你能寫得比別人更好嘛?json
好了,還楞着幹啥,開始幹活。跨域
typeof 能夠正確識別:Undefined、Boolean、Number、String、Symbol、Function 等類型的數據,可是對於其餘的都會認爲是 object,好比 Null、Date 等,因此經過 typeof 來判斷數據類型會不許確。可是可使用 Object.prototype.toString 實現。
function typeOf(obj) {
- let res = Object.prototype.toString.call(obj).split(' ')[1]
- res = res.substring(0, res.length - 1).toLowerCase()
- return res
// 評論區裏提到的更好的寫法
+ return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'
複製代碼
function Animal() {
this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
return this.colors
}
function Dog() {}
Dog.prototype = new Animal()
let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors) // ['black', 'white', 'brown']
複製代碼
原型鏈繼承存在的問題:
function Animal(name) {
this.name = name
this.getName = function() {
return this.name
}
}
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = new Animal()
複製代碼
借用構造函數實現繼承解決了原型鏈繼承的 2 個問題:引用類型共享問題以及傳參問題。可是因爲方法必須定義在構造函數中,因此會致使每次建立子類實例都會建立一遍方法。
組合繼承結合了原型鏈和盜用構造函數,將二者的優勢集中了起來。基本的思路是使用原型鏈繼承原型上的屬性和方法,而經過盜用構造函數繼承實例屬性。這樣既能夠把方法定義在原型上以實現重用,又可讓每一個實例都有本身的屬性。
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2)
// { name: "哈赤", colors: ["black", "white"], age: 1 }
複製代碼
組合繼承已經相對完善了,但仍是存在問題,它的問題就是調用了 2 次父類構造函數,第一次是在 new Animal(),第二次是在 Animal.call() 這裏。
因此解決方案就是不直接調用父類構造函數給子類原型賦值,而是經過建立空函數 F 獲取父類原型的副本。
寄生式組合繼承寫法上和組合繼承基本相似,區別是以下這裏:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f
複製代碼
稍微封裝下上面添加的代碼後:
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(child, parent) {
let prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
inheritPrototype(Dog, Animal)
複製代碼
若是你嫌棄上面的代碼太多了,還能夠基於組合繼承的代碼改爲最簡單的寄生式組合繼承:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ Dog.prototype = Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog
複製代碼
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
複製代碼
ES5 實現:
function unique(arr) {
var res = arr.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return res
}
複製代碼
ES6 實現:
var unique = arr => [...new Set(arr)]
複製代碼
數組扁平化就是將 [1, [2, [3]]] 這種多層的數組拍平成一層 [1, 2, 3]。使用 Array.prototype.flat 能夠直接將多層數組拍平成一層:
[1, [2, [3]]].flat(2) // [1, 2, 3]
複製代碼
如今就是要實現 flat 這種效果。
ES5 實現:遞歸。
function flatten(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
} else {
result.push(arr[i])
}
}
return result;
}
複製代碼
ES6 實現:
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
複製代碼
淺拷貝:只考慮對象類型。
function shallowCopy(obj) {
if (typeof obj !== 'object') return
let newObj = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
複製代碼
簡單版深拷貝:只考慮普通對象屬性,不考慮內置對象和函數。
function deepClone(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
複製代碼
複雜版深克隆:基於簡單版的基礎上,還考慮了內置對象好比 Date、RegExp 等對象和函數以及解決了循環引用的問題。
const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) {
if (map.get(target)) {
return target;
}
// 獲取當前值的構造函數:獲取它的類型
let constructor = target.constructor;
// 檢測當前對象target是否與正則、日期格式對象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)) {
// 建立一個新的特殊對象(正則類/日期類)的實例
return new constructor(target);
}
if (isObject(target)) {
map.set(target, true); // 爲循環引用的對象作標記
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
} else {
return target;
}
}
複製代碼
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 建立副本,若是回調函數內繼續註冊相同事件,會形成死循環
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
// 測試
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布蘭', 12)
// '布蘭 12'
// 'hello, 布蘭 12'
複製代碼
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字符串取出來
const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割後存到數組中
let paramsObj = {};
// 將 params 存到對象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 處理有 value 的參數
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解碼
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉爲數字
if (paramsObj.hasOwnProperty(key)) { // 若是對象有 key,則添加一個值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 若是對象沒有這個 key,建立 key 並設置值
paramsObj[key] = val;
}
} else { // 處理沒有 value 的參數
paramsObj[param] = true;
}
})
return paramsObj;
}
複製代碼
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
if (reg.test(template)) { // 判斷模板裏是否有模板字符串
const name = reg.exec(template)[1]; // 查找當前模板裏第一個模板字符串的字段
template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
return render(template, data); // 遞歸的渲染並返回渲染後的結構
}
return template; // 若是模板沒有模板字符串直接返回
}
複製代碼
測試:
let template = '我是{{name}},年齡{{age}},性別{{sex}}';
let person = {
name: '布蘭',
age: 12
}
render(template, person); // 我是布蘭,年齡12,性別undefined
複製代碼
與普通的圖片懶加載不一樣,以下這個多作了 2 個精心處理:
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
// 修正錯誤,須要加上自執行
- const imgLazyLoad = function() {
+ const imgLazyLoad = (function() {
let count = 0
return function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count++
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
- }
+ })()
// 這裏最好加上防抖處理
document.addEventListener('scroll', imgLazyLoad)
複製代碼
參考:圖片懶加載
觸發高頻事件 N 秒後只會執行一次,若是 N 秒內事件再次觸發,則會從新計時。
簡單版:函數內部支持使用 this 和 event 對象;
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
複製代碼
使用:
var node = document.getElementById('layout')
function getUserAction(e) {
console.log(this, e) // 分別打印:node 這個節點 和 MouseEvent
node.innerHTML = count++;
};
node.onmousemove = debounce(getUserAction, 1000)
複製代碼
最終版:除了支持 this 和 event 外,還支持如下功能:
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 若是已經執行過,再也不執行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
} else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
複製代碼
使用:
var setUseAction = debounce(getUserAction, 10000, true);
// 使用防抖
node.onmousemove = setUseAction
// 取消防抖
setUseAction.cancel()
複製代碼
參考:JavaScript專題之跟着underscore學防抖
觸發高頻事件,且 N 秒內只執行一次。
簡單版:使用時間戳來實現,當即執行一次,而後每 N 秒執行一次。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
複製代碼
最終版:支持取消節流;另外經過傳入第三個參數,options.leading 來表示是否能夠當即執行一次,opitons.trailing 表示結束調用的時候是否還要執行一次,默認都是 true。 注意設置的時候不能同時將 leading 或 trailing 設置爲 false。
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}
複製代碼
節流的使用就不拿代碼舉例了,參考防抖的寫就行。
參考:JavaScript專題之跟着 underscore 學節流
什麼叫函數柯里化?其實就是將使用多個參數的函數轉換成一系列使用一個參數的函數的技術。還不懂?來舉個例子。
function add(a, b, c) {
return a + b + c
}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)
複製代碼
如今就是要實現 curry 這個函數,使函數從一次調用傳入多個參數變成屢次調用每次傳一個參數。
function curry(fn) {
let judge = (...args) => {
if (args.length == fn.length) return fn(...args)
return (...arg) => judge(...args, ...arg)
}
return judge
}
複製代碼
什麼是偏函數?偏函數就是將一個 n 參的函數轉換成固定 x 參的函數,剩餘參數(n - x)將在下次調用所有傳入。舉個例子:
function add(a, b, c) {
return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)
複製代碼
發現沒有,其實偏函數和函數柯里化有點像,因此根據函數柯里化的實現,可以能很快寫出偏函數的實現:
function partial(fn, ...args) {
return (...arg) => {
return fn(...args, ...arg)
}
}
複製代碼
如上這個功能比較簡單,如今咱們但願偏函數能和柯里化同樣能實現佔位功能,好比:
function clg(a, b, c) {
console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3) // 依次打印:1, 2, 3
複製代碼
_
佔的位其實就是 1 的位置。至關於:partial(clg, 1, 2),而後 partialClg(3)。明白了原理,咱們就來寫實現:
function partial(fn, ...args) {
return (...arg) => {
args[index] =
return fn(...args, ...arg)
}
}
複製代碼
JSONP 核心原理:script 標籤不受同源策略約束,因此能夠用來進行跨域請求,優勢是兼容性好,可是隻能用於 GET 請求;
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = ''
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${key}=${params[key]}&`
}
}
dataSrc += `callback=${callbackName}`
return `${url}?${dataSrc}`
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script')
scriptEle.src = generateUrl()
document.body.appendChild(scriptEle)
window[callbackName] = data => {
resolve(data)
document.removeChild(scriptEle)
}
})
}
複製代碼
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
})
}
複製代碼
Array.prototype.forEach2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this) // this 就是當前的數組
const len = O.length >>> 0 // 後面有解釋
let k = 0
while (k < len) {
if (k in O) {
callback.call(thisArg, O[k], k, O);
}
k++;
}
}
複製代碼
O.length >>> 0 是什麼操做?就是無符號右移 0 位,那有什麼意義嘛?就是爲了保證轉換後的值爲正整數。其實底層作了 2 層轉換,第一是非 number 轉成 number 類型,第二是將 number 轉成 Uint32 類型。感興趣能夠閱讀 something >>> 0是什麼意思?。
基於 forEach 的實現可以很容易寫出 map 的實現:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.map2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ res[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}
+ return res
}
複製代碼
一樣,基於 forEach 的實現可以很容易寫出 filter 的實現:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.filter2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ res.push(O[k])
+ }
}
k++;
}
+ return res
}
複製代碼
一樣,基於 forEach 的實現可以很容易寫出 some 的實現:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.some2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ return true
+ }
}
k++;
}
+ return false
}
複製代碼
Array.prototype.reduce2 = function(callback, initialValue) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0, acc
if (arguments.length > 1) {
acc = initialValue
} else {
// 沒傳入初始值的時候,取數組中第一個非 empty 的值爲初始值
while (k < len && !(k in O)) {
k++
}
if (k > len) {
throw new TypeError( 'Reduce of empty array with no initial value' );
}
acc = O[k++]
}
while (k < len) {
if (k in O) {
acc = callback(acc, O[k], k, O)
}
k++
}
return acc
}
複製代碼
使用一個指定的 this 值和一個或多個參數來調用一個函數。
實現要點:
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
複製代碼
apply 和 call 同樣,惟一的區別就是 call 是傳入不固定個數的參數,而 apply 是傳入一個數組。
實現要點:
Function.prototype.apply2 = function (context, arr) {
var context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
複製代碼
bind 方法會建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用。
實現要點:
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
複製代碼
new 運算符用來建立用戶自定義的對象類型的實例或者具備構造函數的內置對象的實例。
實現要點:
function objectFactory() {
var obj = new Object()
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
var ret = Constructor.apply(obj, arguments);
// ret || obj 這裏這麼寫考慮了構造函數顯示返回 null 的狀況
return typeof ret === 'object' ? ret || obj : obj;
};
複製代碼
使用:
function person(name, age) {
this.name = name
this.age = age
}
let p = objectFactory(person, '布蘭', 12)
console.log(p) // { name: '布蘭', age: 12 }
複製代碼
instanceof 就是判斷構造函數的 prototype 屬性是否出如今實例的原型鏈上。
function instanceOf(left, right) {
let proto = left.__proto__
while (true) {
if (proto === null) return false
if (proto === right.prototype) {
return true
}
proto = proto.__proto__
}
}
複製代碼
上面的 left.proto 這種寫法能夠換成 Object.getPrototypeOf(left)。
Object.create()方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。
Object.create2 = function(proto, propertyObject = undefined) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.')
if (propertyObject == null) {
new TypeError('Cannot convert undefined or null to object')
}
function F() {}
F.prototype = proto
const obj = new F()
if (propertyObject != undefined) {
Object.defineProperties(obj, propertyObject)
}
if (proto === null) {
// 建立一個沒有原型對象的對象,Object.create(null)
obj.__proto__ = null
}
return obj
}
複製代碼
Object.assign2 = function(target, ...source) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object')
}
let ret = Object(target)
source.forEach(function(obj) {
if (obj != null) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key]
}
}
}
})
return ret
}
複製代碼
JSON.stringify([, replacer [, space]) 方法是將一個 JavaScript 值(對象或者數組)轉換爲一個 JSON 字符串。此處模擬實現,不考慮可選的第二個參數 replacer 和第三個參數 space,若是對這兩個參數的做用還不瞭解,建議閱讀 MDN 文檔。
function jsonStringify(data) {
let dataType = typeof data;
if (dataType !== 'object') {
let result = data;
//data 多是 string/number/null/undefined/boolean
if (Number.isNaN(data) || data === Infinity) {
//NaN 和 Infinity 序列化返回 "null"
result = "null";
} else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
//function 、undefined 、symbol 序列化返回 undefined
return undefined;
} else if (dataType === 'string') {
result = '"' + data + '"';
}
//boolean 返回 String()
return String(result);
} else if (dataType === 'object') {
if (data === null) {
return "null"
} else if (data.toJSON && typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON());
} else if (data instanceof Array) {
let result = [];
//若是是數組
//toJSON 方法能夠存在於原型鏈中
data.forEach((item, index) => {
if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
result[index] = "null";
} else {
result[index] = jsonStringify(item);
}
});
result = "[" + result + "]";
return result.replace(/'/g, '"');
} else {
//普通對象
/** * 循環引用拋錯(暫未檢測,循環引用時,堆棧溢出) * symbol key 忽略 * undefined、函數、symbol 爲屬性值,被忽略 */
let result = [];
Object.keys(data).forEach((item, index) => {
if (typeof item !== 'symbol') {
//key 若是是symbol對象,忽略
if (data[item] !== undefined && typeof data[item] !== 'function'
&& typeof data[item] !== 'symbol') {
//鍵值若是是 undefined、函數、symbol 爲屬性值,忽略
result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
}
}
});
return ("{" + result + "}").replace(/'/g, '"');
}
}
}
複製代碼
介紹 2 種方法實現:
第一種方式最簡單,也最直觀,就是直接調用 eval,代碼以下:
var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")"); // obj 就是 json 反序列化以後獲得的對象
複製代碼
可是直接調用 eval 會存在安全問題,若是數據中可能不是 json 數據,而是可執行的 JavaScript 代碼,那極可能會形成 XSS 攻擊。所以,在調用 eval 以前,須要對數據進行校驗。
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
rx_one.test(
json.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
複製代碼
Function 與 eval 有相同的字符串參數特性。
var json = '{"name":"小姐姐", "age":20}';
var obj = (new Function('return ' + json))();
複製代碼
實現 Promise 須要徹底讀懂 Promise A+ 規範,不過從整體的實現上看,有以下幾個點須要考慮到:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (value) = > {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach((fn) = > fn());
}
};
let reject = (reason) = > {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach((fn) = > fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 解決 onFufilled,onRejected 沒有傳值的問題
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
// 由於錯誤的值要讓後面訪問到,因此這裏也要拋出錯誤,否則會在以後 then 的 resolve 中捕獲
onRejected = typeof onRejected === "function" ? onRejected : (err) = > {
throw err;
};
// 每次調用 then 都返回一個新的 promise
let promise2 = new Promise((resolve, reject) = > {
if (this.status === FULFILLED) {
//Promise/A+ 2.2.4 --- setTimeout
setTimeout(() = > {
try {
let x = onFulfilled(this.value);
// x多是一個proimise
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === REJECTED) {
//Promise/A+ 2.2.3
setTimeout(() = > {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
const resolvePromise = (promise2, x, resolve, reject) = > {
// 本身等待本身完成是錯誤的實現,用一個類型錯誤,結束掉 promise Promise/A+ 2.3.1
if (promise2 === x) {
return reject(
new TypeError("Chaining cycle detected for promise #<Promise>"));
}
// Promise/A+ 2.3.3.3.3 只能調用一次
let called;
// 後續的條件要嚴格判斷 保證代碼能和別的庫一塊兒使用
if ((typeof x === "object" && x != null) || typeof x === "function") {
try {
// 爲了判斷 resolve 過的就不用再 reject 了(好比 reject 和 resolve 同時調用的時候) Promise/A+ 2.3.3.1
let then = x.then;
if (typeof then === "function") {
// 不要寫成 x.then,直接 then.call 就能夠了 由於 x.then 會再次取值,Object.defineProperty Promise/A+ 2.3.3.3
then.call(
x, (y) = > {
// 根據 promise 的狀態決定是成功仍是失敗
if (called) return;
called = true;
// 遞歸解析的過程(由於可能 promise 中還有 promise) Promise/A+ 2.3.3.3.1
resolvePromise(promise2, y, resolve, reject);
}, (r) = > {
// 只要失敗就失敗 Promise/A+ 2.3.3.3.2
if (called) return;
called = true;
reject(r);
});
} else {
// 若是 x.then 是個普通值就直接返回 resolve 做爲結果 Promise/A+ 2.3.3.4
resolve(x);
}
} catch (e) {
// Promise/A+ 2.3.3.2
if (called) return;
called = true;
reject(e);
}
} else {
// 若是 x 是個普通值就直接返回 resolve 做爲結果 Promise/A+ 2.3.4
resolve(x);
}
};
複製代碼
Promise 寫完以後能夠經過 promises-aplus-tests 這個包對咱們寫的代碼進行測試,看是否符合 A+ 規範。不過測試前還得加一段代碼:
// promise.js
// 這裏是上面寫的 Promise 所有代碼
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
module.exports = Promise;
複製代碼
全局安裝:
npm i promises-aplus-tests -g
複製代碼
終端下執行驗證命令:
promises-aplus-tests promise.js
複製代碼
上面寫的代碼能夠順利經過所有 872 個測試用例。
參考:
Promsie.resolve(value) 能夠將任何值轉成值爲 value 狀態是 fulfilled 的 Promise,但若是傳入的值自己是 Promise 則會原樣返回它。
Promise.resolve = function(value) {
// 若是是 Promsie,則直接輸出它
if(value instanceof Promise){
return value
}
return new Promise(resolve => resolve(value))
}
複製代碼
參考:深刻理解 Promise
和 Promise.resolve() 相似,Promise.reject() 會實例化一個 rejected 狀態的 Promise。但與 Promise.resolve() 不一樣的是,若是給 Promise.reject() 傳遞一個 Promise 對象,則這個對象會成爲新 Promise 的值。
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason))
}
複製代碼
Promise.all 的規則是這樣的:
Promise.all = function(promiseArr) {
let index = 0, result = []
return new Promise((resolve, reject) => {
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
index++
result[i] = val
if (index === promiseArr.length) {
resolve(result)
}
}, err => {
reject(err)
})
})
})
}
複製代碼
Promise.race 會返回一個由全部可迭代實例中第一個 fulfilled 或 rejected 的實例包裝後的新實例。
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
rejecte(err)
})
})
})
}
複製代碼
Promise.allSettled 的規則是這樣:
Promise.allSettled = function(promiseArr) {
let result = []
return new Promise((resolve, reject) => {
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
result.push({
status: 'fulfilled',
value: val
})
if (result.length === promiseArr.length) {
resolve(result)
}
}, err => {
result.push({
status: 'rejected',
reason: err
})
if (result.length === promiseArr.length) {
resolve(result)
}
})
})
})
}
複製代碼
Promise.any 的規則是這樣:
Promise.any = function(promiseArr) {
let index = 0
return new Promise((resolve, reject) => {
if (promiseArr.length === 0) return
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
index++
if (index === promiseArr.length) {
reject(new AggregateError('All promises were rejected'))
}
})
})
})
}
複製代碼
能看到這裏的對代碼都是真愛了,畢竟代碼這玩意看起來是真的很枯燥,可是若是看懂了後,就會像打遊戲贏了同樣開心,並且這玩意會上癮,當你通關了越多的關卡後,你的能力就會拔高一個層次。用標題的話來講就是:搞懂後,提高真的大。加油吧💪,乾飯人
噢不,代碼人。