JavaScript中各種源碼實現(xiàn)(前端面試筆試必備)
前言
最近很多人和我一樣在積極地準備前端的面試筆試,所以我也就整理了一些前端面試筆試中非常容易被問到的原生函數(shù)實現(xiàn)和各種前端原理實現(xiàn)。
能夠手寫實現(xiàn)各種JavaScript原生函數(shù),可以說是擺脫API調(diào)用師帽子的第一步,我們不光要會用,更要去探究其實現(xiàn)原理!
對JavaScript源碼的學習和實現(xiàn)能幫助我們快速和扎實地提升自己的前端編程能力。
實現(xiàn)一個new操作符
我們首先知道new做了什么:
- 創(chuàng)建一個空的簡單JavaScript對象(即{});
- 鏈接該對象(即設(shè)置該對象的構(gòu)造函數(shù))到另一個對象 ;
- 將步驟(1)新創(chuàng)建的對象作為this的上下文 ;
- 如果該函數(shù)沒有返回對象,則返回this。
知道new做了什么,接下來我們就來實現(xiàn)它
- function create(Con, ...args){
- // 創(chuàng)建一個空的對象
- this.obj = {};
- // 將空對象指向構(gòu)造函數(shù)的原型鏈
- Object.setPrototypeOf(this.obj, Con.prototype);
- // obj綁定到構(gòu)造函數(shù)上,便可以訪問構(gòu)造函數(shù)中的屬性,即this.obj.Con(args)
- let result = Con.apply(this.obj, args);
- // 如果返回的result是一個對象則返回
- // new方法失效,否則返回obj
- return result instanceof Object ? result : this.obj;
- }
實現(xiàn)一個Array.isArray
- Array.myIsArray = function(o) {
- return Object.prototype.toString.call(Object(o)) === '[object Array]';
- };
實現(xiàn)一個Object.create()方法
- function create = function (o) {
- var F = function () {};
- F.prototype = o;
- return new F();
- };
實現(xiàn)一個EventEmitter
真實經(jīng)歷,最近在字節(jié)跳動的面試中就被面試官問到了,讓我手寫實現(xiàn)一個簡單的Event類。
- class Event {
- constructor () {
- // 儲存事件的數(shù)據(jù)結(jié)構(gòu)
- // 為查找迅速, 使用對象(字典)
- this._cache = {}
- }
- // 綁定
- on(type, callback) {
- // 為了按類查找方便和節(jié)省空間
- // 將同一類型事件放到一個數(shù)組中
- // 這里的數(shù)組是隊列, 遵循先進先出
- // 即新綁定的事件先觸發(fā)
- 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
- }
- // 觸發(fā)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)
實現(xiàn)一個Array.prototype.reduce
首先觀察一下Array.prototype.reduce語法
- Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
然后就可以動手實現(xiàn)了:
- 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
實現(xiàn)一個call或apply
先來看一個call實例,看看call到底做了什么:
- let foo = {
- value: 1
- };
- function bar() {
- console.log(this.value);
- }
- bar.call(foo); // 1
從代碼的執(zhí)行結(jié)果,我們可以看到,call首先改變了this的指向,使函數(shù)的this指向了foo,然后使bar函數(shù)執(zhí)行了。
總結(jié)一下:
- call改變函數(shù)this指向
- 調(diào)用函數(shù)
思考一下:我們?nèi)绾螌崿F(xiàn)上面的效果呢?代碼改造如下:
- Function.prototype.myCall = function(context) {
- context = context || window;
- //將函數(shù)掛載到對象的fn屬性上
- context.fn = this;
- //處理傳入的參數(shù)
- const args = [...arguments].slice(1);
- //通過對象的屬性調(diào)用該方法
- const result = context.fn(...args);
- //刪除該屬性
- delete context.fn;
- return result
- };
我們看一下上面的代碼:
- 首先我們對參數(shù)context做了兼容處理,不傳值,context默認值為window;
- 然后我們將函數(shù)掛載到context上面,context.fn = this;
- 處理參數(shù),將傳入myCall的參數(shù)截取,去除第一位,然后轉(zhuǎn)為數(shù)組;
- 調(diào)用context.fn,此時fn的this指向context;
- 刪除對象上的屬性 delete context.fn;
- 將結(jié)果返回。
以此類推,我們順便實現(xiàn)一下apply,唯一不同的是參數(shù)的處理,代碼如下:
- Function.prototype.myApply = function(context) {
- context = context || window
- context.fn = this
- let result
- // myApply的參數(shù)形式為(obj,[arg1,arg2,arg3]);
- // 所以myApply的第二個參數(shù)為[arg1,arg2,arg3]
- // 這里我們用擴展運算符來處理一下參數(shù)的傳入方式
- if (arguments[1]) {
- result = context.fn(…arguments[1])
- } else {
- result = context.fn()
- }
- delete context.fn;
- return result
- };
以上便是call和apply的模擬實現(xiàn),唯一不同的是對參數(shù)的處理方式。
實現(xiàn)一個Function.prototype.bind
- function Person(){
- this.name="zs";
- this.age=18;
- this.gender="男"
- }
- let obj={
- hobby:"看書"
- }
- // 將構(gòu)造函數(shù)的this綁定為obj
- let changePerson = Person.bind(obj);
- // 直接調(diào)用構(gòu)造函數(shù),函數(shù)會操作obj對象,給其添加三個屬性;
- changePerson();
- // 1、輸出obj
- console.log(obj);
- // 用改變了this指向的構(gòu)造函數(shù),new一個實例出來
- let p = new changePerson();
- // 2、輸出obj
- console.log(p);
仔細觀察上面的代碼,再看輸出結(jié)果。
我們對Person類使用了bind將其this指向obj,得到了changeperson函數(shù),此處如果我們直接調(diào)用changeperson會改變obj,若用new調(diào)用changeperson會得到實例 p,并且其__proto__指向Person,我們發(fā)現(xiàn)bind失效了。
我們得到結(jié)論:用bind改變了this指向的函數(shù),如果用new操作符來調(diào)用,bind將會失效。
這個對象就是這個構(gòu)造函數(shù)的實例,那么只要在函數(shù)內(nèi)部執(zhí)行 this instanceof 構(gòu)造函數(shù) 來判斷其結(jié)果是否為true,就能判斷函數(shù)是否是通過new操作符來調(diào)用了,若結(jié)果為true則是用new操作符調(diào)用的,代碼修正如下:
- // bind實現(xiàn)
- Function.prototype.mybind = function(){
- // 1、保存函數(shù)
- let _this = this;
- // 2、保存目標對象
- let context = arguments[0]||window;
- // 3、保存目標對象之外的參數(shù),將其轉(zhuǎn)化為數(shù)組;
- let rest = Array.prototype.slice.call(arguments,1);
- // 4、返回一個待執(zhí)行的函數(shù)
- return function F(){
- // 5、將二次傳遞的參數(shù)轉(zhuǎn)化為數(shù)組;
- let rest2 = Array.prototype.slice.call(arguments)
- if(this instanceof F){
- // 6、若是用new操作符調(diào)用,則直接用new 調(diào)用原函數(shù),并用擴展運算符傳遞參數(shù)
- return new _this(...rest2)
- }else{
- //7、用apply調(diào)用第一步保存的函數(shù),并綁定this,傳遞合并的參數(shù)數(shù)組,即context._this(rest.concat(rest2))
- _this.apply(context,rest.concat(rest2));
- }
- }
- };
實現(xiàn)一個JS函數(shù)柯里化
Currying的概念其實并不復雜,用通俗易懂的話說:只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個函數(shù)去處理剩下的參數(shù)。
- 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);
- // 如果參數(shù)個數(shù)小于最初的fn.length,則遞歸調(diào)用,繼續(xù)收集參數(shù)
- if (_args.length < len) {
- return progressCurrying.call(_this, fn, _args);
- }
- // 參數(shù)收集完畢,則執(zhí)行fn
- return fn.apply(this, _args);
- }
- }
手寫防抖(Debouncing)和節(jié)流(Throttling)
節(jié)流
防抖函數(shù) onscroll 結(jié)束時觸發(fā)一次,延遲執(zhí)行
- 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);
節(jié)流
節(jié)流函數(shù) onscroll 時,每隔一段時間觸發(fā)一次,像水滴一樣
- 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;
手寫一個JS深拷貝
乞丐版
- JSON.parse(JSON.stringfy));
非常簡單,但缺陷也很明顯,比如拷貝其他引用類型、拷貝函數(shù)、循環(huán)引用等情況。
基礎(chǔ)版
- function clone(target){
- if(typeof target === 'object'){
- let cloneTarget = {};
- for(const key in target){
- cloneTarget[key] = clone(target[key])
- }
- return cloneTarget;
- } else {
- return target
- }
- }
寫到這里已經(jīng)可以幫助你應(yīng)付一些面試官考察你的遞歸解決問題的能力。但是顯然,這個深拷貝函數(shù)還是有一些問題。
一個比較完整的深拷貝函數(shù),需要同時考慮對象和數(shù)組,考慮循環(huán)引用:
- 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;
- }
- }
實現(xiàn)一個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__;
- }
- }
實現(xiàn)原型鏈繼承
- function myExtend(C, P) {
- var F = function(){};
- F.prototype = P.prototype;
- C.prototype = new F();
- C.prototype.constructor = C;
- C.super = P.prototype;
- }
實現(xiàn)一個async/await
原理
就是利用 generator(生成器)分割代碼片段。然后我們使用一個函數(shù)讓其自迭代,每一個yield 用 promise 包裹起來。執(zhí)行下一步的時機由 promise 來控制
實現(xiàn)
- function _asyncToGenerator(fn) {
- return function() {
- var self = this,
- args = arguments;
- // 將返回值promise化
- return new Promise(function(resolve, reject) {
- // 獲取迭代器實例
- var gen = fn.apply(self, args);
- // 執(zhí)行下一步
- function _next(value) {
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
- }
- // 拋出異常
- function _throw(err) {
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
- }
- // 第一次觸發(fā)
- _next(undefined);
- });
- };
- }
實現(xiàn)一個Array.prototype.flat()函數(shù)
最近字節(jié)跳動的前端面試中也被面試官問到,要求手寫實現(xiàn)。
- 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";
- }
- };
實現(xiàn)一個事件代理
這個問題一般還會讓你講一講事件冒泡和事件捕獲機制
- <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>
實現(xiàn)一個雙向綁定
Vue 2.x的Object.defineProperty版本
- // 數(shù)據(jù)
- const data = {
- text: 'default'
- };
- const input = document.getElementById('input');
- const span = document.getElementById('span');
- // 數(shù)據(jù)劫持
- Object.defineProperty(data, 'text', {
- // 數(shù)據(jù)變化 —> 修改視圖
- set(newVal) {
- input.value = newVal;
- span.innerHTML = newVal;
- }
- });
- // 視圖更改 --> 數(shù)據(jù)變化
- input.addEventListener('keyup', function(e) {
- data.text = e.target.value;
- });
Vue 3.x的proxy 版本
- // 數(shù)據(jù)
- const data = {
- text: 'default'
- };
- const input = document.getElementById('input');
- const span = document.getElementById('span');
- // 數(shù)據(jù)劫持
- const handler = {
- set(target, key, value) {
- target[key] = value;
- // 數(shù)據(jù)變化 —> 修改視圖
- input.value = value;
- span.innerHTML = value;
- return value;
- }
- };
- const proxy = new Proxy(data, handler);
- // 視圖更改 --> 數(shù)據(jù)變化
- input.addEventListener('keyup', function(e) {
- proxy.text = e.target.value;
- });