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

劉小夕發表於2019-07-09

本週面試題一覽:

1. 實現一個 JSON.stringify

JSON.stringify([, replacer [, space]) 方法是將一個JavaScript值(物件或者陣列)轉換為一個 JSON字串。此處模擬實現,不考慮可選的第二個引數 replacer 和第三個引數 space,如果對這兩個引數的作用還不瞭解,建議閱讀 MDN 文件。

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

  1. 基本資料型別:

    • undefined 轉換之後仍是 undefined(型別也是 undefined)
    • boolean 值轉換之後是字串 "false"/"true"
    • number 型別(除了 NaNInfinity)轉換之後是字串型別的數值
    • symbol 轉換之後是 undefined
    • null 轉換之後是字串 "null"
    • string 轉換之後仍是string
    • NaNInfinity 轉換之後是字串 "null"
  2. 如果是函式型別

    • 轉換之後是 undefined
  3. 如果是物件型別(非函式)

    • 如果有 toJSON() 方法,那麼序列化 toJSON() 的返回值。

    • 如果是一個陣列

      • 如果屬性值中出現了 undefined、任意的函式以及 symbol,轉換成字串 "null"
    • 如果是 RegExp 物件。 返回 {} (型別是 string)

    • 如果是 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…

關注公眾號,加入技術交流群

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

相關文章