您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關JavaScript中各種源碼是怎樣實現的,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
最近很多人和我一樣在積極地準備前端的面試筆試,所以我也就整理了一些前端面試筆試中非常容易被問到的原生函數實現和各種前端原理實現。
能夠手寫實現各種JavaScript原生函數,可以說是擺脫API調用師帽子的第一步,我們不光要會用,更要去探究其實現原理!
對JavaScript源碼的學習和實現能幫助我們快速和扎實地提升自己的前端編程能力。
我們首先知道new做了什么:
創建一個空的簡單JavaScript對象(即{});
鏈接該對象(即設置該對象的構造函數)到另一個對象 ;
將步驟(1)新創建的對象作為this的上下文 ;
如果該函數沒有返回對象,則返回this。
知道new做了什么,接下來我們就來實現它
function create(Con, ...args){ // 創建一個空的對象 this.obj = {}; // 將空對象指向構造函數的原型鏈 Object.setPrototypeOf(this.obj, Con.prototype); // obj綁定到構造函數上,便可以訪問構造函數中的屬性,即this.obj.Con(args) let result = Con.apply(this.obj, args); // 如果返回的result是一個對象則返回 // new方法失效,否則返回obj return result instanceof Object ? result : this.obj; }
Array.myIsArray = function(o) { return Object.prototype.toString.call(Object(o)) === '[object Array]'; };
實現一個Object.create()方法
function create = function (o) { var F = function () {}; F.prototype = o; return new F(); };
真實經歷,最近在字節跳動的面試中就被面試官問到了,讓我手寫實現一個簡單的Event類。
class Event { constructor () { // 儲存事件的數據結構 // 為查找迅速, 使用對象(字典) this._cache = {} } // 綁定 on(type, callback) { // 為了按類查找方便和節省空間 // 將同一類型事件放到一個數組中 // 這里的數組是隊列, 遵循先進先出 // 即新綁定的事件先觸發 let fns = (this._cache[type] = this._cache[type] || []) if(fns.indexOf(callback) === -1) { fns.push(callback) } return this } // 解綁 off (type, callback) { let fns = this._cache[type] if(Array.isArray(fns)) { if(callback) { let index = fns.indexOf(callback) if(index !== -1) { fns.splice(index, 1) } } else { // 全部清空 fns.length = 0 } } return this } // 觸發emit trigger(type, data) { let fns = this._cache[type] if(Array.isArray(fns)) { fns.forEach((fn) => { fn(data) }) } return this } // 一次性綁定 once(type, callback) { let wrapFun = () => { callback.call(this); this.off(type, callback); }; this.on(wrapFun, callback); return this; } } let e = new Event() e.on('click',function(){ console.log('on') }) e.on('click',function(){ console.log('onon') }) // e.trigger('click', '666') console.log(e)
首先觀察一下Array.prototype.reduce語法
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
然后就可以動手實現了:
Array.prototype.myReduce = function(callback, initialValue) { let accumulator = initialValue ? initialValue : this[0]; for (let i = initialValue ? 0 : 1; i < this.length; i++) { let _this = this; accumulator = callback(accumulator, this[i], i, _this); } return accumulator; }; // 使用 let arr = [1, 2, 3, 4]; let sum = arr.myReduce((acc, val) => { acc += val; return acc; }, 5); console.log(sum); // 15
先來看一個call實例,看看call到底做了什么:
let foo = { value: 1 }; function bar() { console.log(this.value); } bar.call(foo); // 1
從代碼的執行結果,我們可以看到,call首先改變了this的指向,使函數的this指向了foo,然后使bar函數執行了。
總結一下:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
call改變函數this指向
調用函數
思考一下:我們如何實現上面的效果呢?代碼改造如下:
Function.prototype.myCall = function(context) { context = context || window; //將函數掛載到對象的fn屬性上 context.fn = this; //處理傳入的參數 const args = [...arguments].slice(1); //通過對象的屬性調用該方法 const result = context.fn(...args); //刪除該屬性 delete context.fn; return result };
我們看一下上面的代碼:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
首先我們對參數context做了兼容處理,不傳值,context默認值為window;
然后我們將函數掛載到context上面,context.fn = this;
處理參數,將傳入myCall的參數截取,去除第一位,然后轉為數組;
調用context.fn,此時fn的this指向context;
刪除對象上的屬性 delete context.fn;
將結果返回。
以此類推,我們順便實現一下apply,唯一不同的是參數的處理,代碼如下:
Function.prototype.myApply = function(context) { context = context || window context.fn = this let result // myApply的參數形式為(obj,[arg1,arg2,arg3]); // 所以myApply的第二個參數為[arg1,arg2,arg3] // 這里我們用擴展運算符來處理一下參數的傳入方式 if (arguments[1]) { result = context.fn(…arguments[1]) } else { result = context.fn() } delete context.fn; return result };
以上便是call和apply的模擬實現,唯一不同的是對參數的處理方式。
function Person(){ this.name="zs"; this.age=18; this.gender="男" } let obj={ hobby:"看書" } // 將構造函數的this綁定為obj let changePerson = Person.bind(obj); // 直接調用構造函數,函數會操作obj對象,給其添加三個屬性; changePerson(); // 1、輸出obj console.log(obj); // 用改變了this指向的構造函數,new一個實例出來 let p = new changePerson(); // 2、輸出obj console.log(p);
仔細觀察上面的代碼,再看輸出結果。
我們對Person類使用了bind將其this指向obj,得到了changeperson函數,此處如果我們直接調用changeperson會改變obj,若用new調用changeperson會得到實例 p,并且其__proto__指向Person,我們發現bind失效了。
我們得到結論:用bind改變了this指向的函數,如果用new操作符來調用,bind將會失效。
這個對象就是這個構造函數的實例,那么只要在函數內部執行 this instanceof 構造函數 來判斷其結果是否為true,就能判斷函數是否是通過new操作符來調用了,若結果為true則是用new操作符調用的,代碼修正如下:
// bind實現 Function.prototype.mybind = function(){ // 1、保存函數 let _this = this; // 2、保存目標對象 let context = arguments[0]||window; // 3、保存目標對象之外的參數,將其轉化為數組; let rest = Array.prototype.slice.call(arguments,1); // 4、返回一個待執行的函數 return function F(){ // 5、將二次傳遞的參數轉化為數組; let rest2 = Array.prototype.slice.call(arguments) if(this instanceof F){ // 6、若是用new操作符調用,則直接用new 調用原函數,并用擴展運算符傳遞參數 return new _this(...rest2) }else{ //7、用apply調用第一步保存的函數,并綁定this,傳遞合并的參數數組,即context._this(rest.concat(rest2)) _this.apply(context,rest.concat(rest2)); } } };
Currying的概念其實并不復雜,用通俗易懂的話說:只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
function progressCurrying(fn, args) { let _this = this let len = fn.length; let args = args || []; return function() { let _args = Array.prototype.slice.call(arguments); Array.prototype.push.apply(args, _args); // 如果參數個數小于最初的fn.length,則遞歸調用,繼續收集參數 if (_args.length < len) { return progressCurrying.call(_this, fn, _args); } // 參數收集完畢,則執行fn return fn.apply(this, _args); } }
節流
防抖函數 onscroll 結束時觸發一次,延遲執行
function debounce(func, wait) { let timeout; return function() { let context = this; // 指向全局 let args = arguments; if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { func.apply(context, args); // context.func(args) }, wait); }; } // 使用 window.onscroll = debounce(function() { console.log('debounce'); }, 1000);
節流
節流函數 onscroll 時,每隔一段時間觸發一次,像水滴一樣
function throttle(fn, delay) { let prevTime = Date.now(); return function() { let curTime = Date.now(); if (curTime - prevTime > delay) { fn.apply(this, arguments); prevTime = curTime; } }; } // 使用 var throtteScroll = throttle(function() { console.log('throtte'); }, 1000); window.onscroll = throtteScroll;
乞丐版
JSON.parse(JSON.stringfy));
非常簡單,但缺陷也很明顯,比如拷貝其他引用類型、拷貝函數、循環引用等情況。
基礎版
function clone(target){ if(typeof target === 'object'){ let cloneTarget = {}; for(const key in target){ cloneTarget[key] = clone(target[key]) } return cloneTarget; } else { return target } }
寫到這里已經可以幫助你應付一些面試官考察你的遞歸解決問題的能力。但是顯然,這個深拷貝函數還是有一些問題。
一個比較完整的深拷貝函數,需要同時考慮對象和數組,考慮循環引用:
function clone(target, map = new WeakMap()) { if(typeof target === 'object'){ let cloneTarget = Array.isArray(target) ? [] : {}; if(map.get(target)) { return target; } map.set(target, cloneTarget); for(const key in target) { cloneTarget[key] = clone(target[key], map) } return cloneTarget; } else { return target; } }
原理: 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__; } }
function myExtend(C, P) { var F = function(){}; F.prototype = P.prototype; C.prototype = new F(); C.prototype.constructor = C; C.super = P.prototype; }
原理
就是利用 generator(生成器)分割代碼片段。然后我們使用一個函數讓其自迭代,每一個yield 用 promise 包裹起來。執行下一步的時機由 promise 來控制
實現
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); }); }; }
最近字節跳動的前端面試中也被面試官問到,要求手寫實現。
Array.prototype.myFlat = function(num = 1) { if (Array.isArray(this)) { let arr = []; if (!Number(num) || Number(num) < 0) { return this; } this.forEach(item => { if(Array.isArray(item)){ let count = num arr = arr.concat(item.myFlat(--count)) } else { arr.push(item) } }); return arr; } else { throw tihs + ".flat is not a function"; } };
這個問題一般還會讓你講一講事件冒泡和事件捕獲機制
<ul id="color-list"> <li>red</li> <li>yellow</li> <li>blue</li> <li>green</li> <li>black</li> <li>white</li> </ul> <script> (function () { var color_list = document.getElementById('color-list'); color_list.addEventListener('click', showColor, true); function showColor(e) { var x = e.target; if (x.nodeName.toLowerCase() === 'li') { alert(x.innerHTML); } } })(); </script>
Vue 2.x的Object.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; });
Vue 3.x的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, handler); // 視圖更改 --> 數據變化 input.addEventListener('keyup', function(e) { proxy.text = e.target.value; });
上述就是小編為大家分享的JavaScript中各種源碼是怎樣實現的了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。