本週面試題一覽:
1. 實現一個 JSON.stringify
JSON.stringify([, replacer [, space])
方法是將一個JavaScript值(物件或者陣列)轉換為一個 JSON字串。此處模擬實現,不考慮可選的第二個引數 replacer
和第三個引數 space
,如果對這兩個引數的作用還不瞭解,建議閱讀 MDN 文件。
JSON.stringify()
將值轉換成對應的JSON
格式:
-
基本資料型別:
- undefined 轉換之後仍是 undefined(型別也是
undefined
) - boolean 值轉換之後是字串
"false"/"true"
- number 型別(除了
NaN
和Infinity
)轉換之後是字串型別的數值 - symbol 轉換之後是
undefined
- null 轉換之後是字串
"null"
- string 轉換之後仍是string
NaN
和Infinity
轉換之後是字串"null"
- undefined 轉換之後仍是 undefined(型別也是
-
如果是函式型別
- 轉換之後是
undefined
- 轉換之後是
-
如果是物件型別(非函式)
-
如果有
toJSON()
方法,那麼序列化toJSON()
的返回值。 -
如果是一個陣列
- 如果屬性值中出現了
undefined
、任意的函式以及symbol
,轉換成字串"null"
- 如果屬性值中出現了
-
如果是
RegExp
物件。 返回{}
(型別是 string) -
如果是
Date
物件,返回Date
的toJSON
字串值 -
如果是普通物件;
- 如果屬性值中出現了
undefined
、任意的函式以及 symbol 值,忽略。 - 所有以
symbol
為屬性鍵的屬性都會被完全忽略掉。
- 如果屬性值中出現了
-
-
對包含迴圈引用的物件(物件之間相互引用,形成無限迴圈)執行此方法,會丟擲錯誤。
模擬實現
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
Function
與 eval
有相同的字串引數特性。
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-align
和 line-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
複製程式碼
模組輸出的值是複雜資料型別
- 模組輸出的是物件,屬性值是簡單資料型別時:
//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
關聯,default
和 hobbies
中儲存的是同一個物件的堆記憶體地址,當這個物件的值發生改變時,此時 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
[5] 釋出訂閱模式與觀察者模式
謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。 github.com/YvetteLau/B…