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

不積跬步無以至千里。

關於【Step-By-Step】

Step-By-Step (點擊進入項目) 是我於 2019-05-20 開始的一個項目,每個工作日發佈一道面試題。

每個週末我會仔細閱讀大家的答案,整理最一份較優答案出來,因本人水平有限,有誤的地方,大家及時指正。

如果想 加羣 學習,可以通過文末的公衆號,添加我爲好友。

__

本週面試題一覽:

31. 實現一個 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));

32. 實現一個 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))();

33. 實現一個觀察者模式

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

觀察者(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有困難時,會自動通知給他們(觀察者)

34. 使用 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;
}

35. 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] 珠峯架構課(牆裂推薦)
[2] JSON.parse三種實現方式
[3] ES6 文檔
[4] JSON-js
[5] CommonJS模塊和ES6模塊的區別
[6] 發佈訂閱模式與觀察者模式

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章