原文地址:https://github.com/YvetteLau/Blog/issues/38

关于【Step-By-Step】

Step-By-Step (点击进入项目) 是我于

2019-05-20 开始的一个项目,每个工作日发布一道面试题。

每个周末我会仔细阅读大家的答案,整理最一份较优答案出来,因本人水平有限,有误的地方,大家及时指正。


本周面试题一览:

1. 实现一个 JSON.stringify

JSON.stringify([, replacer [, space]) 方法是将一个JavaScript值(对象或者数组)转换为一个
JSON字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space,如果对这两个参数的作用还不了解,建议阅读
MDN
文档。

JSON.stringify() 将值转换成对应的 JSON 格式:

  1. 基本数据类型:
  • undefined 转换之后仍是 undefined(类型也是 undefined)
  • boolean 值转换之后是字符串 "false"/"true"
  • number 类型(除了 NaN 和 Infinity)转换之后是字符串类型的数值
  • symbol 转换之后是 undefined
  • null 转换之后是字符串 "null"
  • string 转换之后仍是string
  • NaN 和 Infinity 转换之后是字符串 "null"
  1. 如果是函数类型
  • 转换之后是 undefined
  1. 如果是对象类型(非函数)
  • 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。

  • 如果是一个数组

  • 如果属性值中出现了 undefined、任意的函数以及 symbol,转换成字符串 "null"

  • 如果是 RegExp 对象。

返回 {} (类型是 string)

  • 如果是 Date 对象,返回 Date 的 toJSON 字符串值

  • 如果是普通对象;

  • 如果属性值中出现了 undefined、任意的函数以及 symbol 值,忽略。

  • 所有以 symbol 为属性键的属性都会被完全忽略掉。

  1. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。

模拟实现

functionjsonStringify(data) {
    let dataType =typeof data;
    if (dataType !=='object') {
        let result = data;
        //data 可能是 string/number/null/undefined/booleanif (Number.isNaN(data) || data ===Infinity) {
            //NaN 和 Infinity 序列化返回 "null"
            result ="null";
        } elseif (dataType ==='function'|| dataType ==='undefined'|| dataType ==='symbol') {
            //function 、undefined 、symbol 序列化返回 undefinedreturnundefined;
        } elseif (dataType ==='string') {
            result ='"'+ data +'"';
        }
        //boolean 返回 String()returnString(result);
    } elseif (dataType ==='object') {
        if (data ===null) {
            return"null";
        } elseif (data.toJSON&&typeofdata.toJSON==='function') {
            returnjsonStringify(data.toJSON());
        } elseif (data instanceofArray) {
            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 +"]";
            returnresult.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 =newRegExp("\w");
console.log(jsonStringify(reg) ===JSON.stringify(reg));
let date =newDate();
console.log(jsonStringify(date) ===JSON.stringify(date));
let obj = {
    name: '刘小夕',
    age: 22,
    hobbie: ['coding', 'writing'],
    date: newDate(),
    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));

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

functionSubType(info) {
    this.info= info;
}
SubType.prototype.toJSON=function () {
    return {
        name: '钱钱钱',
        mount: 'many',
        say: function () {
            console.log('我偏不说!');
        },
        more: null,
        reg: newRegExp("\w")
    }
}
let sub =newSubType('hi');
console.log(jsonStringify(sub) ===JSON.stringify(sub));
let map =newMap();
map.set('name', '小姐姐');
console.log(jsonStringify(map) ===JSON.stringify(map));
let set =newSet([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 = (newFunction('return '+ json))();

3. 实现一个观察者模式

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。

观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

//有一家猎人工会,其中每个猎人都具有发布任务(publish),订阅任务(subscribe)的功能//他们都有一个订阅列表来记录谁订阅了自己//定义一个猎人类//包括姓名,级别,订阅列表functionHunter(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 =newHunter('小明', '黄金')
    let hunterJin =newHunter('小金', '白银')
    let hunterZhang =newHunter('小张', '黄金')
    let hunterPeter =newHunter('Peter', '青铜')

    //Peter等级较低,可能需要帮助,所以小明,小金,小张都订阅了PeterhunterMing.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.jslet name ='William';
setTimeout(() => { name ='Yvette'; }, 300);
module.exports= name;

//index.jsconstname=require('./name');
console.log(name); //William//name.js 模块加载后,它的内部变化就影响不到 name//name 是一个基本数据类型。将其复制出一份之后,二者之间互不影响。setTimeout(() =>console.log(name), 500); //William

模块输出的值是复杂数据类型

  1. 模块输出的是对象,属性值是简单数据类型时:

    //name.jslet name =’William’;
    setTimeout(() => { name =’Yvette’; }, 300);
    module.exports= { name };

    //index.jsconst { name } =require(‘./name’);
    console.log(name); //William//name 是一个原始类型的值,会被缓存。setTimeout(() =>console.log(name), 500); //William

模块输出的是对象:

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

//index.jsconst { name, hobbies } =require('./name');
console.log(name); //Williamconsole.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);//Williamconsole.log(hobbies);//['coding', 'reading']
}, 500);

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

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

//index.jsimport { 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.jslet name ='William';
let hobbies = ['coding']
setTimeout(() => { name ='Yvette'; hobbies.push('writing'); }, 300);
exportdefault { name, hobbies };

//index.jsimportinfofrom'./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 模块的输入变量是只读的,不能对其进行重新赋值

importnamefrom'./name';
name ='Star'; //抛错

6. 当使用require命令加载某个模块时,就会运行整个模块的代码。

7. 当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。