【Step-By-Step】高頻面試題深刻解析 / 週刊07

本週面試題一覽:javascript

1. 實現一個 JSON.stringify

JSON.stringify([, replacer [, space]) 方法是將一個JavaScript值(對象或者數組)轉換爲一個 JSON字符串。此處模擬實現,不考慮可選的第二個參數 replacer 和第三個參數 space,若是對這兩個參數的做用還不瞭解,建議閱讀 MDN 文檔。css

JSON.stringify() 將值轉換成對應的 JSON 格式:html

  1. 基本數據類型:java

    • undefined 轉換以後還是 undefined(類型也是 undefined)
    • boolean 值轉換以後是字符串 "false"/"true"
    • number 類型(除了 NaNInfinity)轉換以後是字符串類型的數值
    • symbol 轉換以後是 undefined
    • null 轉換以後是字符串 "null"
    • string 轉換以後還是string
    • NaNInfinity 轉換以後是字符串 "null"
  2. 若是是函數類型webpack

    • 轉換以後是 undefined
  3. 若是是對象類型(非函數)git

    • 若是有 toJSON() 方法,那麼序列化 toJSON() 的返回值。es6

    • 若是是一個數組github

      • 若是屬性值中出現了 undefined、任意的函數以及 symbol,轉換成字符串 "null"
    • 若是是 RegExp 對象。 返回 {} (類型是 string)web

    • 若是是 Date 對象,返回 DatetoJSON 字符串值面試

    • 若是是普通對象;

      • 若是屬性值中出現了 undefined、任意的函數以及 symbol 值,忽略。
      • 全部以 symbol 爲屬性鍵的屬性都會被徹底忽略掉。
  4. 對包含循環引用的對象(對象之間相互引用,造成無限循環)執行此方法,會拋出錯誤。

模擬實現

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, '"');
        }
    }
}
複製代碼

測試代碼:

let sym = Symbol(10);
console.log(jsonStringify(sym) === JSON.stringify(sym));
let nul = null;
console.log(jsonStringify(nul) === JSON.stringify(nul));
let und = undefined;
console.log(jsonStringify(undefined) === JSON.stringify(undefined));
let boo = false;
console.log(jsonStringify(boo) === JSON.stringify(boo));
let nan = NaN;
console.log(jsonStringify(nan) === JSON.stringify(nan));
let inf = Infinity;
console.log(jsonStringify(Infinity) === JSON.stringify(Infinity));
let str = "hello";
console.log(jsonStringify(str) === JSON.stringify(str));
let reg = new RegExp("\w");
console.log(jsonStringify(reg) === JSON.stringify(reg));
let date = new Date();
console.log(jsonStringify(date) === JSON.stringify(date));
let obj = {
    name: '劉小夕',
    age: 22,
    hobbie: ['coding', 'writing'],
    date: new Date(),
    unq: Symbol(10),
    sayHello: function () {
        console.log("hello")
    },
    more: {
        brother: 'Star',
        age: 20,
        hobbie: [null],
        info: {
            money: undefined,
            job: null,
            others: []
        }
    }
}
console.log(jsonStringify(obj) === JSON.stringify(obj));


function SuperType(name, age) {
    this.name = name;
    this.age = age;
}
let per = new SuperType('小姐姐', 20);
console.log(jsonStringify(per) === JSON.stringify(per));

function SubType(info) {
    this.info = info;
}
SubType.prototype.toJSON = function () {
    return {
        name: '錢錢錢',
        mount: 'many',
        say: function () {
            console.log('我偏不說!');
        },
        more: null,
        reg: new RegExp("\w")
    }
}
let sub = new SubType('hi');
console.log(jsonStringify(sub) === JSON.stringify(sub));
let map = new Map();
map.set('name', '小姐姐');
console.log(jsonStringify(map) === JSON.stringify(map));
let set = new Set([1, 2, 3, 4, 5, 1, 2, 3]);
console.log(jsonStringify(set) === JSON.stringify(set));
複製代碼

2. 實現一個 JSON.parse

JSON.parse(JSON.parse(text[, reviver]) 方法用來解析JSON字符串,構造由字符串描述的JavaScript值或對象。提供可選的reviver函數用以在返回以前對所獲得的對象執行變換。此處模擬實現,不考慮可選的第二個參數 reviver ,若是對這個參數的做用還不瞭解,建議閱讀 MDN 文檔。

第一種方式 eval

最簡單,最直觀的方式就是調用 eval

var json = '{"name":"小姐姐", "age":20}';
var obj = eval("(" + json + ")");  // obj 就是 json 反序列化以後獲得的對象
複製代碼

直接調用 eval 存在 XSS 漏洞,數據中可能不是 json 數據,而是可執行的 JavaScript 代碼。所以,在調用 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 + ")");
}
複製代碼

JSON 是 JS 的子集,能夠直接交給 eval 運行。

第二種方式 new Function

Functioneval 有相同的字符串參數特性。

var json = '{"name":"小姐姐", "age":20}';
var obj = (new Function('return ' + json))();
複製代碼

3. 實現一個觀察者模式

觀察者模式定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知,並自動更新。觀察者模式屬於行爲型模式,行爲型模式關注的是對象之間的通信,觀察者模式就是觀察者和被觀察者之間的通信。

觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當主題被激活的時候,會觸發(Fire Event)觀察者裏的事件。

//有一家獵人工會,其中每一個獵人都具備發佈任務(publish),訂閱任務(subscribe)的功能
    //他們都有一個訂閱列表來記錄誰訂閱了本身
    //定義一個獵人類
    //包括姓名,級別,訂閱列表
    function Hunter(name, level){
        this.name = name
        this.level = level
        this.list = []
    }
    Hunter.prototype.publish = function (money){
        console.log(this.level + '獵人' + this.name + '尋求幫助')
        this.list.forEach(function(item, index){
            item(money)
        })
    }
    Hunter.prototype.subscribe = function (targrt, fn){
        console.log(this.level + '獵人' + this.name + '訂閱了' + targrt.name)
        targrt.list.push(fn)
    }
    
    //獵人工會走來了幾個獵人
    let hunterMing = new Hunter('小明', '黃金')
    let hunterJin = new Hunter('小金', '白銀')
    let hunterZhang = new Hunter('小張', '黃金')
    let hunterPeter = new Hunter('Peter', '青銅')
    
    //Peter等級較低,可能須要幫助,因此小明,小金,小張都訂閱了Peter
    hunterMing.subscribe(hunterPeter, function(money){
        console.log('小明表示:' + (money > 200 ? '' : '暫時很忙,不能') + '給予幫助')
    });
    hunterJin.subscribe(hunterPeter, function(){
        console.log('小金表示:給予幫助')
    });
    hunterZhang.subscribe(hunterPeter, function(){
        console.log('小張表示:給予幫助')
    });
    
    //Peter遇到困難,賞金198尋求幫助
    hunterPeter.publish(198);
    
    //獵人們(觀察者)關聯他們感興趣的獵人(目標對象),如Peter,當Peter有困難時,會自動通知給他們(觀察者)
複製代碼

4. 使用 CSS 讓一個元素水平垂直居中

父元素 .container

子元素 .box

利用 flex 佈局

/* 無需知道被居中元素的寬高 */
.container {
    display: flex;
    align-items: center;
    justify-content: center;
}
複製代碼

子元素是單行文本

設置父元素的 text-alignline-height = height

.container {
    height: 100px;
    line-height: 100px;
    text-align: center;
}
複製代碼

利用 absolute + transform

/* 無需知道被居中元素的寬高 */
/* 設置父元素非 `static` 定位 */
.container {
    position: relative;
}
/* 子元素絕對定位,使用 translate的好處是無需知道子元素的寬高 */
/* 若是知道寬高,也可使用 margin 設置 */
.box {
    position: absolute;
    left: -50%;
    top: -50%;
    transform: translate(-50%, -50%);
}
複製代碼

利用 grid 佈局

/* 無需知道被居中元素的寬高 */
.container {
    display: grid;
}
.box {
    justify-self: center; 
    align-self: center;
}
複製代碼

利用絕對定位和 margin:auto

/* 無需知道被居中元素的寬高 */
.box {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
}
.container {
    position: relative;
}
複製代碼

5. ES6模塊和 CommonJS 模塊有哪些差別?

1. CommonJS 模塊是運行時加載,ES6模塊是編譯時輸出接口。
  • ES6模塊在編譯時,就能肯定模塊的依賴關係,以及輸入和輸出的變量。ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
  • CommonJS 加載的是一個對象,該對象只有在腳本運行完纔會生成。
2. CommonJS 模塊輸出的是一個值的拷貝,ES6模塊輸出的是值的引用。
- `CommonJS` 輸出的是一個值的拷貝(注意基本數據類型/複雜數據類型)
    
- ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。
複製代碼

CommonJS 模塊輸出的是值的拷貝。

模塊輸出的值是基本數據類型,模塊內部的變化就影響不到這個值。

//name.js
let name = 'William';
setTimeout(() => { name = 'Yvette'; }, 300);
module.exports = name;

//index.js
const name = require('./name');
console.log(name); //William
//name.js 模塊加載後,它的內部變化就影響不到 name
//name 是一個基本數據類型。將其複製出一份以後,兩者之間互不影響。
setTimeout(() => console.log(name), 500); //William
複製代碼

模塊輸出的值是複雜數據類型

  1. 模塊輸出的是對象,屬性值是簡單數據類型時:
//name.js
let name = 'William';
setTimeout(() => { name = 'Yvette'; }, 300);
module.exports = { name };

//index.js
const { name } = require('./name');
console.log(name); //William
//name 是一個原始類型的值,會被緩存。
setTimeout(() => console.log(name), 500); //William
複製代碼

模塊輸出的是對象:

//name.js
let name = 'William';
let hobbies = ['coding'];
setTimeout(() => { 
    name = 'Yvette';
    hobbies.push('reading');
}, 300);
module.exports = { name, hobbies };

//index.js
const { name, hobbies } = require('./name');
console.log(name); //William
console.log(hobbies); //['coding']
/* * name 的值沒有受到影響,由於 {name: name} 屬性值 name 存的是個字符串 * 300ms後 name 變量從新賦值,可是不會影響 {name: name} * * hobbies 的值會被影響,由於 {hobbies: hobbies} 屬性值 hobbies 中存的是 * 數組的堆內存地址,所以當 hobbies 對象的值被改變時,存在棧內存中的地址並 沒有發生變化,所以 hoobies 對象值的改變會影響 {hobbies: hobbies} * xx = { name, hobbies } 也所以改變 (複雜數據類型,拷貝的棧內存中存的地址) */
setTimeout(() => {
    console.log(name);//William
    console.log(hobbies);//['coding', 'reading']
}, 500);
複製代碼

ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令 import ,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。

//name.js
let name = 'William';
setTimeout(() => { name = 'Yvette'; hobbies.push('writing'); }, 300);
export { name };
export var hobbies = ['coding'];

//index.js
import { name, hobbies } from './name';
console.log(name, hobbies); //William ["coding"]
//name 和 hobbie 都會被模塊內部的變化所影響
setTimeout(() => {
    console.log(name, hobbies); //Yvette ["coding", "writing"]
}, 500); //Yvette
複製代碼

ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。所以上面的例子也很容易理解。

那麼 export default 導出是什麼狀況呢?

//name.js
let name = 'William';
let hobbies = ['coding']
setTimeout(() => { name = 'Yvette'; hobbies.push('writing'); }, 300);
export default { name, hobbies };

//index.js
import info from './name';
console.log(info.name, info.hobbies); //William ["coding"]
//name 不會被模塊內部的變化所影響
//hobbie 會被模塊內部的變化所影響
setTimeout(() => {
    console.log(info.name, info.hobbies); //William ["coding", "writing"]
}, 500); //Yvette
複製代碼

一塊兒看一下爲何。

export default 能夠理解爲將變量賦值給 default,最後導出 default (僅是方便理解,不表明最終的實現,若是對這塊感興趣,能夠閱讀 webpack 編譯出來的代碼)。

基礎類型變量 name, 賦值給 default 以後,只讀引用與 default 關聯,此時原變量 name 的任何修改都與 default 無關。

複雜數據類型變量 hobbies,賦值給 default以後,只讀引用與 default 關聯,defaulthobbies 中存儲的是同一個對象的堆內存地址,當這個對象的值發生改變時,此時 default 的值也會發生變化。

3. ES6 模塊自動採用嚴格模式,不管模塊頭部是否寫了 "use strict";
4. require 能夠作動態加載,import 語句作不到,import 語句必須位於頂層做用域中。
5. ES6 模塊的輸入變量是隻讀的,不能對其進行從新賦值
import name from './name';
name = 'Star'; //拋錯
複製代碼
6. 當使用require命令加載某個模塊時,就會運行整個模塊的代碼。
7. 當使用require命令加載同一個模塊時,不會再執行該模塊,而是取到緩存之中的值。也就是說,CommonJS模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。

參考文章:

[1] JSON.parse三種實現方式

[2] ES6 文檔

[3] JSON-js

[4] CommonJS模塊和ES6模塊的區別

[5] 發佈訂閱模式與觀察者模式

謝謝各位小夥伴願意花費寶貴的時間閱讀本文,若是本文給了您一點幫助或者是啓發,請不要吝嗇你的贊和Star,您的確定是我前進的最大動力。 github.com/YvetteLau/B…

關注公衆號,加入技術交流羣

相關文章
相關標籤/搜索