新聞中心
為什么要寫這類文章
作為一個程序員,代碼能力毋庸置疑是非常非常重要的,就像現(xiàn)在為什么大廠面試基本都問什么 API 怎么實現(xiàn)可見其重要性。我想說的是居然手寫這么重要,那我們就必須掌握它,所以文章標(biāo)題用了死磕,一點也不過分,也希望不被認(rèn)為是標(biāo)題黨。

成都創(chuàng)新互聯(lián)服務(wù)項目包括廣陵網(wǎng)站建設(shè)、廣陵網(wǎng)站制作、廣陵網(wǎng)頁制作以及廣陵網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,廣陵網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到廣陵省份的部分城市,未來相信會繼續(xù)擴大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
作為一個普通前端,我是真的寫不出 Promise A+ 規(guī)范,但是沒關(guān)系,我們可以站在巨人的肩膀上,要相信我們現(xiàn)在要走的路,前人都走過,所以可以找找現(xiàn)在社區(qū)已經(jīng)存在的那些優(yōu)秀的文章,比如工業(yè)聚大佬寫的 100 行代碼實現(xiàn) Promises/A+ 規(guī)范,找到這些文章后不是收藏夾吃灰,得找個時間踏踏實實的學(xué),一行一行的磨,直到搞懂為止。我現(xiàn)在就是這么干的。
能收獲什么
這篇文章總體上分為 2 類手寫題,前半部分可以歸納為是常見需求,后半部分則是對現(xiàn)有技術(shù)的實現(xiàn);
- 對常用的需求進行手寫實現(xiàn),比如數(shù)據(jù)類型判斷函數(shù)、深拷貝等可以直接用于往后的項目中,提高了項目開發(fā)效率;
- 對現(xiàn)有關(guān)鍵字和 API 的實現(xiàn),可能需要用到別的知識或 API,比如在寫 forEach 的時候用到了無符號位右移的操作,平時都不怎么能夠接觸到這玩意,現(xiàn)在遇到了就可以順手把它掌握了。所以手寫這些實現(xiàn)能夠潛移默化的擴展并鞏固自己的 JS 基礎(chǔ);
- 通過寫各種測試用例,你會知道各種 API 的邊界情況,比如 Promise.all, 你得考慮到傳入?yún)?shù)的各種情況,從而加深了對它們的理解及使用;
閱讀的時候需要做什么
閱讀的時候,你需要把每行代碼都看懂,知道它在干什么,為什么要這么寫,能寫得更好嘛?比如在寫圖片懶加載的時候,一般我們都是根據(jù)當(dāng)前元素的位置和視口進行判斷是否要加載這張圖片,普通程序員寫到這就差不多完成了。而大佬程序員則是會多考慮一些細(xì)節(jié)的東西,比如性能如何更優(yōu)?代碼如何更精簡?比如 yeyan1996 寫的圖片懶加載就多考慮了 2 點:比如圖片全部加載完成的時候得把事件監(jiān)聽給移除;比如加載完一張圖片的時候,得把當(dāng)前 img 從 imgList 里移除,起到優(yōu)化內(nèi)存的作用。
除了讀通代碼之外,還可以打開 Chrome 的 Script snippet 去寫測試用例跑跑代碼,做到更好的理解以及使用。
在看了幾篇以及寫了很多測試用例的前提下,嘗試自己手寫實現(xiàn),看看自己到底掌握了多少。條條大路通羅馬,你還能有別的方式實現(xiàn)嘛?或者你能寫得比別人更好嘛?
好了,還楞著干啥,開始干活。
數(shù)據(jù)類型判斷
typeof 可以正確識別:Undefined、Boolean、Number、String、Symbol、Function 等類型的數(shù)據(jù),但是對于其他的都會認(rèn)為是 object,比如 Null、Date 等,所以通過 typeof 來判斷數(shù)據(jù)類型會不準(zhǔn)確。但是可以使用 Object.prototype.toString 實現(xiàn)。
- function typeOf(obj) {
- let res = Object.prototype.toString.call(obj).split(' ')[1]
- res = res.substring(0, res.length - 1).toLowerCase()
- return res
- }
- 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']
原型鏈繼承存在的問題:
- 問題1:原型中包含的引用類型屬性將被所有實例共享;
- 問題2:子類在實例化的時候不能給父類構(gòu)造函數(shù)傳參;
借用構(gòu)造函數(shù)實現(xiàn)繼承
- function Animal(name) {
- this.name = name
- this.getName = function() {
- return this.name
- }
- }
- function Dog(name) {
- Animal.call(this, name)
- }
- Dog.prototype = new Animal()
借用構(gòu)造函數(shù)實現(xiàn)繼承解決了原型鏈繼承的 2 個問題:引用類型共享問題以及傳參問題。但是由于方法必須定義在構(gòu)造函數(shù)中,所以會導(dǎo)致每次創(chuàng)建子類實例都會創(chuàng)建一遍方法。
組合繼承
組合繼承結(jié)合了原型鏈和盜用構(gòu)造函數(shù),將兩者的優(yōu)點集中了起來。基本的思路是使用原型鏈繼承原型上的屬性和方法,而通過盜用構(gòu)造函數(shù)繼承實例屬性。這樣既可以把方法定義在原型上以實現(xiàn)重用,又可以讓每個實例都有自己的屬性。
- 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 }
寄生式組合繼承
組合繼承已經(jīng)相對完善了,但還是存在問題,它的問題就是調(diào)用了 2 次父類構(gòu)造函數(shù),第一次是在 new Animal(),第二次是在 Animal.call() 這里。
所以解決方案就是不直接調(diào)用父類構(gòu)造函數(shù)給子類原型賦值,而是通過創(chuàng)建空函數(shù) F 獲取父類原型的副本。
寄生式組合繼承寫法上和組合繼承基本類似,區(qū)別是如下這里:
- - 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 實現(xiàn)繼承
- class Animal {
- constructor(name) {
- this.name = name
- }
- getName() {
- return this.name
- }
- }
- class Dog extends Animal {
- constructor(name, age) {
- super(name)
- this.age = age
- }
- }
數(shù)組去重
ES5 實現(xiàn):
- function unique(arr) {
- var res = arr.filter(function(item, index, array) {
- return array.indexOf(item) === index
- })
- return res
- }
ES6 實現(xiàn):
- var unique = arr => [...new Set(arr)]
數(shù)組扁平化
數(shù)組扁平化就是將 [1, [2, [3]]] 這種多層的數(shù)組拍平成一層 [1, 2, 3]。使用 Array.prototype.flat 可以直接將多層數(shù)組拍平成一層:
- [1, [2, [3]]].flat(2) // [1, 2, 3]
現(xiàn)在就是要實現(xiàn) flat 這種效果。
ES5 實現(xiàn):遞歸。
- 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 實現(xiàn):
- 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
- }
簡單版深拷貝:只考慮普通對象屬性,不考慮內(nèi)置對象和函數(shù)。
- 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;
- }
復(fù)雜版深克?。夯诤唵伟娴幕A(chǔ)上,還考慮了內(nèi)置對象比如 Date、RegExp 等對象和函數(shù)以及解決了循環(huán)引用的問題。
- const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
- function deepClone(target, map = new WeakMap()) {
- if (map.get(target)) {
- return target;
- }
- // 獲取當(dāng)前值的構(gòu)造函數(shù):獲取它的類型
- let constructor = target.constructor;
- // 檢測當(dāng)前對象target是否與正則、日期格式對象匹配
- if (/^(RegExp|Date)$/i.test(constructor.name)) {
- // 創(chuàng)建一個新的特殊對象(正則類/日期類)的實例
- return new constructor(target);
- }
- if (isObject(target)) {
- map.set(target, true); // 為循環(huán)引用的對象做標(biāo)記
- 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;
- }
- }
事件總線(發(fā)布訂閱模式)
- 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]) {
- // 創(chuàng)建副本,如果回調(diào)函數(shù)內(nèi)繼續(xù)注冊相同事件,會造成死循環(huán)
- 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'
解析 URL 參數(shù)為對象
- function parseParam(url) {
- const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 后面的字符串取出來
- const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割后存到數(shù)組中
- let paramsObj = {};
- // 將 params 存到對象中
- paramsArr.forEach(param => {
- if (/=/.test(param)) { // 處理有 value 的參數(shù)
- let [key, val] = param.split('='); // 分割 key 和 value
- val = decodeURIComponent(val); // 解碼
- val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉(zhuǎn)為數(shù)字
- if (paramsObj.hasOwnProperty(key)) { // 如果對象有 key,則添加一個值
- paramsObj[key] = [].concat(paramsObj[key], val);
- } else { // 如果對象沒有這個 key,創(chuàng)建 key 并設(shè)置值
- paramsObj[key] = val;
- }
- } else { // 處理沒有 value 的參數(shù)
- paramsObj[param] = true;
- }
- })
- return paramsObj;
- }
字符串模板
- function render(template, data) {
- const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
- if (reg.test(template)) { // 判斷模板里是否有模板字符串
- const name = reg.exec(template)[1]; // 查找當(dāng)前模板里第一個模板字符串的字段
- template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
- return render(template, data); // 遞歸的渲染并返回渲染后的結(jié)構(gòu)
- }
- return template; // 如果模板沒有模板字符串直接返回
- }
測試:
- let template = '我是{{name}},年齡{{age}},性別{{sex}}';
- let person = {
- name: '布蘭',
- age: 12
- }
- render(template, person); // 我是布蘭,年齡12,性別undefined
圖片懶加載
與普通的圖片懶加載不同,如下這個多做了 2 個精心處理:
- 圖片全部加載完成后移除事件監(jiān)聽;
- 加載完的圖片,從 imgList 移除;
- let imgList = [...document.querySelectorAll('img')]
- let length = imgList.length
- 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)
函數(shù)防抖
觸發(fā)高頻事件 N 秒后只會執(zhí)行一次,如果 N 秒內(nèi)事件再次觸發(fā),則會重新計時。
簡單版:函數(shù)內(nèi)部支持使用 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 這個節(jié)點 和 MouseEvent
- node.innerHTML = count++;
- };
- node.onmousemove = debounce(getUserAction, 1000)
最終版:除了支持 this 和 event 外,還支持以下功能:
- 支持立即執(zhí)行;
- 函數(shù)可能有返回值;
- 支持取消功能;
- function debounce(func, wait, immediate) {
- var timeout, result;
- var debounced = function () {
- var context = this;
- var args = arguments;
- if (timeout) clearTimeout(timeout);
- if (immediate) {
- // 如果已經(jīng)執(zhí)行過,不再執(zhí)行
- 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學(xué)防抖
函數(shù)節(jié)流
觸發(fā)高頻事件,且 N 秒內(nèi)只執(zhí)行一次。
簡單版:使用時間戳來實現(xiàn),立即執(zhí)行一次,然后每 N 秒執(zhí)行一次。
- 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;
- }
- }
- }
最終版:支持取消節(jié)流;另外通過傳入第三個參數(shù),options.leading 來表示是否可以立即執(zhí)行一次,opitons.trailing 表示結(jié)束調(diào)用的時候是否還要執(zhí)行一次,默認(rèn)都是 true。注意設(shè)置的時候不能同時將 leading 或 trailing 設(shè)置為 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;
- }
節(jié)流的使用就不拿代碼舉例了,參考防抖的寫就行。
函數(shù)柯里化
什么叫函數(shù)柯里化?其實就是將使用多個參數(shù)的函數(shù)轉(zhuǎn)換成一系列使用一個參數(shù)的函數(shù)的技術(shù)。還不懂?來舉個例子。
- function add(a, b, c) {
- return a + b + c
- }
- add(1, 2, 3)
- let addCurry = curry(add)
- addCurry(1)(2)(3)
現(xiàn)在就是要實現(xiàn) curry 這個函數(shù),使函數(shù)從一次調(diào)用傳入多個參數(shù)變成多次調(diào)用每次傳一個參數(shù)。
- function curry(fn) {
- let judge = (...args) => {
- if (args.length == fn.length) return fn(...args)
- return (...arg) => judge(...args, ...arg)
- }
- return judge
- }
偏函數(shù)
什么是偏函數(shù)?偏函數(shù)就是將一個 n 參的函數(shù)轉(zhuǎn)換成固定 x 參的函數(shù),剩余參數(shù)(n - x)將在下次調(diào)用全部傳入。舉個例子:
- function add(a, b, c) {
- return a + b + c
- }
- let partialAdd = partial(add, 1)
- partialAdd(2, 3)
發(fā)現(xiàn)沒有,其實偏函數(shù)和函數(shù)柯里化有點像,所以根據(jù)函數(shù)柯里化的實現(xiàn),能夠能很快寫出偏函數(shù)的實現(xiàn):
- function partial(fn, ...args) {
- return (...arg) => {
- return fn(...args, ...arg)
- }
- }
如上這個功能比較簡單,現(xiàn)在我們希望偏函數(shù)能和柯里化一樣能實現(xiàn)占位功能,比如:
- function clg(a, b, c) {
- console.log(a, b, c)
- }
- let partialClg = partial(clg, '_', 2)
- partialClg(1, 3) // 依次打?。?, 2, 3
_ 占的位其實就是 1 的位置。相當(dāng)于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我們就來寫實現(xiàn):
- function partial(fn, ...args) {
- return (...arg) => {
- args[index] =
- return fn(...args, ...arg)
- }
- }
JSONP
JSONP 核心原理:script 標(biāo)簽不受同源策略約束,所以可以用來進行跨域請求,優(yōu)點是兼容性好,但是只能用于 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)
- }
- })
- }
AJAX
- 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();
- })
- }
實現(xiàn)數(shù)組原型方法
forEach
- 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 就是當(dāng)前的數(shù)組
- 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 位,那有什么意義嘛?就是為了保證轉(zhuǎn)換后的值為正整數(shù)。其實底層做了 2 層轉(zhuǎn)換,第一是非 number 轉(zhuǎn)成 number 類型,第二是將 number 轉(zhuǎn)成 Uint32 類型。感興趣可以閱讀 something >>> 0是什么意思?[3]。
map
基于 forEach 的實現(xiàn)能夠很容易寫出 map 的實現(xiàn):
- - Array.prototype.forEach2 = function(callback,
新聞名稱:死磕36個JS手寫題,搞懂后提升真的大
文章URL:http://m.fisionsoft.com.cn/article/cdjcjog.html


咨詢
建站咨詢
