共计 9412 个字符,预计需要花费 24 分钟才能阅读完成。
不积跬步无以至千里。
关于【Step-By-Step】
Step-By-Step (点击进入项目) 是我于
2019-05-20
开始的一个项目,每个工作日发布一道面试题。每个周末我会仔细阅读大家的答案,整理最一份较优答案出来,因本人水平有限,有误的地方,大家及时指正。
如果想 加群 学习,可以通过文末的公众号,添加我为好友。
__
本周面试题一览:
- 实现一个 JSON.stringify
- 实现一个 JSON.parse
- 实现一个观察者模式
- 使用 CSS 让一个元素水平垂直居中有哪些方式
- ES6 模块和 CommonJS 模块有哪些差异?
31. 实现一个 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));
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
Function
与 eval
有相同的字符串参数特性。
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-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;}
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
模块输出的值是复杂数据类型
- 模块输出的是对象,属性值是简单数据类型时:
//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] 珠峰架构课(墙裂推荐)
[2] JSON.parse 三种实现方式
[3] ES6 文档
[4] JSON-js
[5] CommonJS 模块和 ES6 模块的区别
[6] 发布订阅模式与观察者模式
谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和 Star,您的肯定是我前进的最大动力。https://github.com/YvetteLau/…
关注公众号,加入技术交流群。