关于前端:前端二面必会面试题附答案

55次阅读

共计 22242 个字符,预计需要花费 56 分钟才能阅读完成。

typeof NaN 的后果是什么?

NaN 指“不是一个数字”(not a number),NaN 是一个“戒备值”(sentinel value,有非凡用处的惯例值),用于指出数字类型中的谬误状况,即“执行数学运算没有胜利,这是失败后返回的后果”。

typeof NaN; // "number"

NaN 是一个非凡值,它和本身不相等,是惟一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。

new 操作符的实现原理

new 操作符的执行过程:

(1)首先创立了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象增加属性)

(4)判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象。

具体实现:

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回后果
  return flag ? result : newObject;
}
// 应用办法
objectFactory(构造函数, 初始化参数);

写代码:实现函数可能深度克隆根本类型

浅克隆:

function shallowClone(obj) {let cloneObj = {};

  for (let i in obj) {cloneObj[i] = obj[i];
  }

  return cloneObj;
}

深克隆:

  • 思考根底类型
  • 援用类型

    • RegExp、Date、函数 不是 JSON 平安的
    • 会失落 constructor,所有的构造函数都指向 Object
    • 破解循环援用
function deepCopy(obj) {if (typeof obj === 'object') {var result = obj.constructor === Array ? [] : {};

    for (var i in obj) {result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];
    }
  } else {var result = obj;}

  return result;
}

你在工作终于到那些问题,解决办法是什么

常常遇到的问题就是 Cannot read property‘prototype’of undefined
解决办法通过浏览器报错提醒代码定位问题,解决问题

Vue 我的项目中遇到视图不更新,办法不执行,埋点不触发等问题
个别解决方案查看浏览器报错,查看代码运行到那个阶段未之行完结,浏览源码以及相干文档等
而后举进去最近开发的我的项目中遇到的算是两个比拟大的问题。

浏览器渲染优化

(1)针对 JavaScript: JavaScript 既会阻塞 HTML 的解析,也会阻塞 CSS 的解析。因而咱们能够对 JavaScript 的加载形式进行扭转,来进行优化:

(1)尽量将 JavaScript 文件放在 body 的最初

(2)body 两头尽量不要写 <script> 标签

(3)<script>标签的引入资源形式有三种,有一种就是咱们罕用的间接引入,还有两种就是应用 async 属性和 defer 属性来异步引入,两者都是去异步加载内部的 JS 文件,不会阻塞 DOM 的解析(尽量应用异步加载)。三者的区别如下:

  • script 立刻进行页面渲染去加载资源文件,当资源加载结束后立刻执行 js 代码,js 代码执行结束后持续渲染页面;
  • async 是在下载实现之后,立刻异步加载,加载好后立刻执行,多个带 async 属性的标签,不能保障加载的程序;
  • defer 是在下载实现之后,立刻异步加载。加载好后,如果 DOM 树还没构建好,则先等 DOM 树解析好再执行;如果 DOM 树曾经筹备好,则立刻执行。多个带 defer 属性的标签,依照程序执行。

(2)针对 CSS:应用 CSS 有三种形式:应用link、@import、内联款式,其中 link 和 @import 都是导入内部款式。它们之间的区别:

  • link:浏览器会派发一个新等线程 (HTTP 线程) 去加载资源文件,与此同时 GUI 渲染线程会持续向下渲染代码
  • @import:GUI 渲染线程会临时进行渲染,去服务器加载资源文件,资源文件没有返回之前不会持续渲染(妨碍浏览器渲染)
  • style:GUI 间接渲染

内部款式如果长时间没有加载结束,浏览器为了用户体验,会应用浏览器会默认款式,确保首次渲染的速度。所以 CSS 个别写在 headr 中,让浏览器尽快发送申请去获取 css 款式。

所以,在开发过程中,导入内部款式应用 link,而不必 @import。如果 css 少,尽可能采纳内嵌款式,间接写在 style 标签中。

(3)针对 DOM 树、CSSOM 树: 能够通过以下几种形式来缩小渲染的工夫:

  • HTML 文件的代码层级尽量不要太深
  • 应用语义化的标签,来防止不规范语义化的非凡解决
  • 缩小 CSSD 代码的层级,因为选择器是从左向右进行解析的

(4)缩小回流与重绘:

  • 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
  • 不要应用 table 布局,一个小的改变可能会使整个 table 进行从新布局
  • 应用 CSS 的表达式
  • 不要频繁操作元素的款式,对于动态页面,能够批改类名,而不是款式。
  • 应用 absolute 或者 fixed,使元素脱离文档流,这样他们发生变化就不会影响其余元素
  • 防止频繁操作 DOM,能够创立一个文档片段documentFragment,在它下面利用所有 DOM 操作,最初再把它增加到文档中
  • 将元素先设置display: none,操作完结后再把它显示进去。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 将 DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于 浏览器的渲染队列机制

浏览器针对页面的回流与重绘,进行了本身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了肯定的数量或者到了肯定的工夫距离,浏览器就会对队列进行批处理。这样就会让屡次的回流、重绘变成一次回流重绘。

将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,本来应该是触发屡次回流,变成了只触发一次回流。

CSS 选择器及其优先级

选择器 格局 优先级权重
id 选择器 #id 100
类选择器 #classname 10
属性选择器 a[ref=“eee”] 10
伪类选择器 li:last-child 10
标签选择器 div 1
伪元素选择器 li:after 1
相邻兄弟选择器 h1+p 0
子选择器 ul>li 0
后辈选择器 li a 0
通配符选择器 * 0

对于选择器的 优先级

  • 标签选择器、伪元素选择器:1
  • 类选择器、伪类选择器、属性选择器:10
  • id 选择器:100
  • 内联款式:1000

注意事项:

  • !important 申明的款式的优先级最高;
  • 如果优先级雷同,则最初呈现的款式失效;
  • 继承失去的款式的优先级最低;
  • 通用选择器(*)、子选择器(>)和相邻同胞选择器(+)并不在这四个等级中,所以它们的权值都为 0;
  • 样式表的起源不同时,优先级程序为:内联款式 > 外部款式 > 内部款式 > 浏览器用户自定义款式 > 浏览器默认款式。

Promise 是什么,解决了什么,之前怎么实现的

    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更正当和更弱小。解决来之前在申请中回调申请产生的回调天堂,使得当初的代码更加正当更加优雅,也更加容易定位查找问题。

组件之间的传值有几种形式

1、父传子
2、子传父
3、eventbus
4、ref/$refs
5、$parent/$children
6、$attrs/$listeners
7、依赖注入(provide/inject)

TCP 的重传机制

因为 TCP 的上层网络(网络层)可能呈现 失落、反复或失序 的状况,TCP 协定提供牢靠数据传输服务。为保障数据传输的正确性,TCP 会重传其认为已失落(包含报文中的比特谬误)的包。TCP 应用两套独立的机制来实现重传,一是 基于工夫 ,二是 基于确认信息

TCP 在发送一个数据之后,就开启一个定时器,若是在这个工夫内没有收到发送数据的 ACK 确认报文,则对该报文进行重传,在达到肯定次数还没有胜利时放弃并发送一个复位信号。

JSONP

JSONP 外围原理:script 标签不受同源策略束缚,所以能够用来进行跨域申请,长处是兼容性好,然而只能用于 GET 申请;

const jsonp = ({url, params, callbackName}) => {const generateUrl = () => {
        let dataSrc = ''
        for (let key in params) {if (params.hasOwnProperty(key)) {dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`
        return `${url}?${dataSrc}`
    }
    return new Promise((resolve, reject) => {const scriptEle = document.createElement('script')
        scriptEle.src = generateUrl()
        document.body.appendChild(scriptEle)
        window[callbackName] = data => {resolve(data)
            document.removeChild(scriptEle)
        }
    })
}

什么是 margin 重叠问题?如何解决?

问题形容: 两个块级元素的上外边距和下外边距可能会合并(折叠)为一个外边距,其大小会取其中外边距值大的那个,这种行为就是外边距折叠。须要留神的是,浮动的元素和相对定位 这种脱离文档流的元素的外边距不会折叠。重叠只会呈现在 垂直方向

计算准则: 折叠合并后外边距的计算准则如下:

  • 如果两者都是负数,那么就去最大者
  • 如果是一正一负,就会正值减去负值的绝对值
  • 两个都是负值时,用 0 减去两个中绝对值大的那个

解决办法: 对于折叠的状况,次要有两种:兄弟之间重叠 父子之间重叠(1)兄弟之间重叠

  • 底部元素变为行内盒子:display: inline-block
  • 底部元素设置浮动:float
  • 底部元素的 position 的值为absolute/fixed

(2)父子之间重叠

  • 父元素退出:overflow: hidden
  • 父元素增加通明边框:border:1px solid transparent
  • 子元素变为行内盒子:display: inline-block
  • 子元素退出浮动属性或定位

浏览器的渲染过程

浏览器渲染次要有以下步骤:

  • 首先解析收到的文档,依据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
  • 而后对 CSS 进行解析,生成 CSSOM 规定树。
  • 依据 DOM 树和 CSSOM 规定树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个蕴含有色彩和大小等属性的矩形,渲染对象和 DOM 元素绝对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们个别是一些具备简单构造的元素,无奈用一个矩形来形容。
  • 当渲染对象被创立并增加到树中,它们并没有地位和大小,所以当浏览器生成渲染树当前,就会依据渲染树来进行布局(也能够叫做回流)。这一阶段浏览器要做的事件是要弄清楚各个节点在页面中的确切地位和大小。通常这一行为也被称为“主动重排”。
  • 布局阶段完结后是绘制阶段,遍历渲染树并调用渲染对象的 paint 办法将它们的内容显示在屏幕上,绘制应用 UI 根底组件。

大抵过程如图所示:

留神: 这个过程是逐渐实现的,为了更好的用户体验,渲染引擎将会尽可能早的将内容出现到屏幕上,并不会等到所有的 html 都解析实现之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

实现 JSONP 跨域

JSONP 外围原理script 标签不受同源策略束缚,所以能够用来进行跨域申请,长处是兼容性好,然而只能用于 GET 申请;

实现

const jsonp = (url, params, callbackName) => {const generateUrl = () => {
        let dataSrc = "";
        for(let key in params) {if(params.hasOwnProperty(key)) {dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`;
        return `${url}?${dataSrc}`;
    }
    return new Promise((resolve, reject) => {const scriptEle = document.createElement('script');
        scriptEle.src = generateUrl();
        document.body.appendChild(scriptEle);
        window[callbackName] = data => {resolve(data);
            document.removeChild(scriptEle);
        }
    });
}

深拷贝(思考到复制 Symbol 类型)

题目形容: 手写 new 操作符实现

实现代码如下:

function isObject(val) {return typeof val === "object" && val !== null;}

function deepClone(obj, hash = new WeakMap()) {if (!isObject(obj)) return obj;
  if (hash.has(obj)) {return hash.get(obj);
  }
  let target = Array.isArray(obj) ? [] : {};
  hash.set(obj, target);
  Reflect.ownKeys(obj).forEach((item) => {if (isObject(obj[item])) {target[item] = deepClone(obj[item], hash);
    } else {target[item] = obj[item];
    }
  });

  return target;
}

// var obj1 = {
// a:1,
// b:{a:2}
// };
// var obj2 = deepClone(obj1);
// console.log(obj1);

OSI 七层模型

ISO为了更好的使网络应用更为遍及,推出了 OSI 参考模型。

(1)应用层

OSI参考模型中最靠近用户的一层,是为计算机用户提供利用接口,也为用户间接提供各种网络服务。咱们常见应用层的网络服务协定有:HTTPHTTPSFTPPOP3SMTP等。

  • 在客户端与服务器中常常会有数据的申请,这个时候就是会用到 http(hyper text transfer protocol)(超文本传输协定) 或者https. 在后端设计数据接口时,咱们经常应用到这个协定。
  • FTP是文件传输协定,在开发过程中,集体并没有波及到,然而我想,在一些资源网站,比方 百度网盘` 迅雷 ` 应该是基于此协定的。
  • SMTPsimple mail transfer protocol(简略邮件传输协定)。在一个我的项目中,在用户邮箱验证码登录的性能时,应用到了这个协定。

(2)表示层

表示层提供各种用于应用层数据的编码和转换性能, 确保一个零碎的应用层发送的数据能被另一个零碎的应用层辨认。如果必要,该层可提供一种规范示意模式,用于将计算机外部的多种数据格式转换成通信中采纳的规范示意模式。数据压缩和加密也是表示层可提供的转换性能之一。

在我的项目开发中,为了不便数据传输,能够应用 base64 对数据进行编解码。如果按性能来划分,base64应该是工作在表示层。

(3)会话层

会话层就是负责建设、治理和终止表示层实体之间的通信会话。该层的通信由不同设施中的应用程序之间的服务申请和响应组成。

(4)传输层

传输层建设了主机端到端的链接,传输层的作用是为下层协定提供端到端的牢靠和通明的数据传输服务,包含解决差错控制和流量管制等问题。该层向高层屏蔽了上层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户管制和设定的、牢靠的数据通路。咱们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

(5)网络层

本层通过 IP 寻址来建设两个节点之间的连贯,为源端的运输层送来的分组,抉择适合的路由和替换节点,正确无误地依照地址传送给目标端的运输层。就是通常说的 IP 层。这一层就是咱们常常说的 IP 协定层。IP协定是 Internet 的根底。咱们能够这样了解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。

(6)数据链路层

将比特组合成字节, 再将字节组合成帧, 应用链路层地址 (以太网应用 MAC 地址)来拜访介质, 并进行过错检测。
网络层与数据链路层的比照,通过下面的形容,咱们或者能够这样了解,网络层是布局了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还减少了差错控制的性能。

(7)物理层

理论最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。罕用设施有(各种物理设施)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

OSI 七层模型通信特点:对等通信 对等通信,为了使数据分组从源传送到目的地,源端 OSI 模型的每一层都必须与目标端的对等层进行通信,这种通信形式称为对等层通信。在每一层通信过程中,应用本层本人协定进行通信。

实现函数原型办法

call

应用一个指定的 this 值和一个或多个参数来调用一个函数。

实现要点:

  • this 可能传入 null;
  • 传入不固定个数的参数;
  • 函数可能有返回值;
Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

apply

apply 和 call 一样,惟一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。

实现要点:

  • this 可能传入 null;
  • 传入一个数组;
  • 函数可能有返回值;
Function.prototype.apply2 = function (context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) {result = context.fn();
    } else {var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

bind

bind 办法会创立一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时应用。

实现要点:

  • bind() 除了 this 外,还可传入多个参数;
  • bing 创立的新函数可能传入多个参数;
  • 新函数可能被当做结构函数调用;
  • 函数可能有返回值;
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

实现 new 关键字

new 运算符用来创立用户自定义的对象类型的实例或者具备构造函数的内置对象的实例。

实现要点:

  • new 会产生一个新对象;
  • 新对象须要可能拜访到构造函数的属性,所以须要从新指定它的原型;
  • 构造函数可能会显示返回;
function objectFactory() {var obj = new Object()
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    var ret = Constructor.apply(obj, arguments);

    // ret || obj 这里这么写思考了构造函数显示返回 null 的状况
    return typeof ret === 'object' ? ret || obj : obj;
};

应用:

function person(name, age) {
    this.name = name
    this.age = age
}
let p = objectFactory(person, '布兰', 12)
console.log(p)  // {name: '布兰', age: 12}

实现 instanceof 关键字

instanceof 就是判断构造函数的 prototype 属性是否呈现在实例的原型链上。

function instanceOf(left, right) {
    let proto = left.__proto__
    while (true) {if (proto === null) return false
        if (proto === right.prototype) {return true}
        proto = proto.__proto__
    }
}

下面的 left.proto 这种写法能够换成 Object.getPrototypeOf(left)。

实现 Object.create

Object.create()办法创立一个新对象,应用现有的对象来提供新创建的对象的__proto__。

Object.create2 = function(proto, propertyObject = undefined) {if (typeof proto !== 'object' && typeof proto !== 'function') {throw new TypeError('Object prototype may only be an Object or null.')
    if (propertyObject == null) {new TypeError('Cannot convert undefined or null to object')
    }
    function F() {}
    F.prototype = proto
    const obj = new F()
    if (propertyObject != undefined) {Object.defineProperties(obj, propertyObject)
    }
    if (proto === null) {// 创立一个没有原型对象的对象,Object.create(null)
        obj.__proto__ = null
    }
    return obj
}

实现 Object.assign

Object.assign2 = function(target, ...source) {if (target == null) {throw new TypeError('Cannot convert undefined or null to object')
    }
    let ret = Object(target) 
    source.forEach(function(obj) {if (obj != null) {for (let key in obj) {if (obj.hasOwnProperty(key)) {ret[key] = obj[key]
                }
            }
        }
    })
    return ret
}

实现 JSON.stringify

JSON.stringify([, replacer [, space]) 办法是将一个 JavaScript 值 (对象或者数组) 转换为一个 JSON 字符串。此处模仿实现,不思考可选的第二个参数 replacer 和第三个参数 space

  1. 根本数据类型:

    • undefined 转换之后仍是 undefined(类型也是 undefined)
    • boolean 值转换之后是字符串 “false”/”true”
    • number 类型 (除了 NaN 和 Infinity) 转换之后是字符串类型的数值
    • symbol 转换之后是 undefined
    • null 转换之后是字符串 “null”
    • string 转换之后仍是 string
    • NaN 和 Infinity 转换之后是字符串 “null”
  2. 函数类型:转换之后是 undefined
  3. 如果是对象类型(非函数)

    • 如果是一个数组:如果属性值中呈现了 undefined、任意的函数以及 symbol,转换成字符串 “null”;
    • 如果是 RegExp 对象:返回 {} (类型是 string);
    • 如果是 Date 对象,返回 Date 的 toJSON 字符串值;
    • 如果是一般对象;

      • 如果有 toJSON() 办法,那么序列化 toJSON() 的返回值。
      • 如果属性值中呈现了 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,'"');
        }
    }
}

实现 JSON.parse

介绍 2 种办法实现:

  • eval 实现;
  • new Function 实现;

eval 实现

第一种形式最简略,也最直观,就是间接调用 eval,代码如下:

var json = '{"a":"1","b":2}';
var obj = eval("(" + json + ")");  // obj 就是 json 反序列化之后失去的对象

然而间接调用 eval 会存在平安问题,如果数据中可能不是 json 数据,而是可执行的 JavaScript 代码,那很可能会造成 XSS 攻打。因而,在调用 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 + ")");
}

new Function 实现

Function 与 eval 有雷同的字符串参数个性。

var json = '{"name":" 小姐姐 ","age":20}';
var obj = (new Function('return' + json))();

实现 Promise

实现 Promise 须要齐全读懂 Promise A+ 标准,不过从总体的实现上看,有如下几个点须要思考到:

  • then 须要反对链式调用,所以得返回一个新的 Promise;
  • 解决异步问题,所以得先用 onResolvedCallbacks 和 onRejectedCallbacks 别离把胜利和失败的回调存起来;
  • 为了让链式调用失常进行上来,须要判断 onFulfilled 和 onRejected 的类型;
  • onFulfilled 和 onRejected 须要被异步调用,这里用 setTimeout 模仿异步;
  • 解决 Promise 的 resolve;
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];

        let resolve = (value) = > {if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                this.onResolvedCallbacks.forEach((fn) = > fn());
            }
        };

        let reject = (reason) = > {if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                this.onRejectedCallbacks.forEach((fn) = > fn());
            }
        };

        try {executor(resolve, reject);
        } catch (error) {reject(error);
        }
    }

    then(onFulfilled, onRejected) {
        // 解决 onFufilled,onRejected 没有传值的问题
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v;
        // 因为谬误的值要让前面拜访到,所以这里也要抛出谬误,不然会在之后 then 的 resolve 中捕捉
        onRejected = typeof onRejected === "function" ? onRejected : (err) = > {throw err;};
        // 每次调用 then 都返回一个新的 promise
        let promise2 = new Promise((resolve, reject) = > {if (this.status === FULFILLED) {
                //Promise/A+ 2.2.4 --- setTimeout
                setTimeout(() = > {
                    try {let x = onFulfilled(this.value);
                        // x 可能是一个 proimise
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {reject(e);
                    }
                }, 0);
            }

            if (this.status === REJECTED) {
                //Promise/A+ 2.2.3
                setTimeout(() = > {
                    try {let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {reject(e);
                    }
                }, 0);
            }

            if (this.status === PENDING) {this.onResolvedCallbacks.push(() = > {setTimeout(() = > {
                        try {let x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {reject(e);
                        }
                    }, 0);
                });

                this.onRejectedCallbacks.push(() = > {setTimeout(() = > {
                        try {let x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {reject(e);
                        }
                    }, 0);
                });
            }
        });

        return promise2;
    }
}
const resolvePromise = (promise2, x, resolve, reject) = > {
    // 本人期待本人实现是谬误的实现,用一个类型谬误,完结掉 promise  Promise/A+ 2.3.1
    if (promise2 === x) {
        return reject(new TypeError("Chaining cycle detected for promise #<Promise>"));
    }
    // Promise/A+ 2.3.3.3.3 只能调用一次
    let called;
    // 后续的条件要严格判断 保障代码能和别的库一起应用
    if ((typeof x === "object" && x != null) || typeof x === "function") {
        try {
            // 为了判断 resolve 过的就不必再 reject 了(比方 reject 和 resolve 同时调用的时候)Promise/A+ 2.3.3.1
            let then = x.then;
            if (typeof then === "function") {
            // 不要写成 x.then,间接 then.call 就能够了 因为 x.then 会再次取值,Object.defineProperty  Promise/A+ 2.3.3.3
                then.call(x, (y) = > {
                        // 依据 promise 的状态决定是胜利还是失败
                        if (called) return;
                        called = true;
                        // 递归解析的过程(因为可能 promise 中还有 promise)Promise/A+ 2.3.3.3.1
                        resolvePromise(promise2, y, resolve, reject);
                    }, (r) = > {
                        // 只有失败就失败 Promise/A+ 2.3.3.3.2
                        if (called) return;
                        called = true;
                        reject(r);
                    });
            } else {
                // 如果 x.then 是个一般值就间接返回 resolve 作为后果  Promise/A+ 2.3.3.4
                resolve(x);
            }
        } catch (e) {
            // Promise/A+ 2.3.3.2
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        // 如果 x 是个一般值就间接返回 resolve 作为后果  Promise/A+ 2.3.4
        resolve(x);
    }
};

Promise 写完之后能够通过 promises-aplus-tests 这个包对咱们写的代码进行测试,看是否合乎 A+ 标准。不过测试前还得加一段代码:

// promise.js
// 这里是下面写的 Promise 全副代码
Promise.defer = Promise.deferred = function () {let dfd = {}
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}
module.exports = Promise;

全局装置:

npm i promises-aplus-tests -g

终端下执行验证命令:

promises-aplus-tests promise.js

下面写的代码能够顺利通过全副 872 个测试用例。

Promise.resolve

Promsie.resolve(value) 能够将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值自身是 Promise 则会原样返回它。

Promise.resolve = function(value) {
    // 如果是 Promsie,则间接输入它
    if(value instanceof Promise){return value}
    return new Promise(resolve => resolve(value))
}

Promise.reject

和 Promise.resolve() 相似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。

Promise.reject = function(reason) {return new Promise((resolve, reject) => reject(reason))
}

Promise.all

Promise.all 的规定是这样的:

  • 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;
  • 只有有一个 Promise 是 rejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejected 的 Promise 的值;
  • 只有有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise;
Promise.all = function(promiseArr) {let index = 0, result = []
    return new Promise((resolve, reject) => {promiseArr.forEach((p, i) => {Promise.resolve(p).then(val => {
                index++
                result[i] = val
                if (index === promiseArr.length) {resolve(result)
                }
            }, err => {reject(err)
            })
        })
    })
}

Promise.race

Promise.race 会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例。

Promise.race = function(promiseArr) {return new Promise((resolve, reject) => {
        promiseArr.forEach(p => {Promise.resolve(p).then(val => {resolve(val)
            }, err => {rejecte(err)
            })
        })
    })
}

Promise.allSettled

Promise.allSettled 的规定是这样:

  • 所有 Promise 的状态都变动了,那么新返回一个状态是 fulfilled 的 Promise,且它的值是一个数组,数组的每项由所有 Promise 的值和状态组成的对象;
  • 如果有一个是 pending 的 Promise,则返回一个状态是 pending 的新实例;
Promise.allSettled = function(promiseArr) {let result = []

    return new Promise((resolve, reject) => {promiseArr.forEach((p, i) => {Promise.resolve(p).then(val => {
                result.push({
                    status: 'fulfilled',
                    value: val
                })
                if (result.length === promiseArr.length) {resolve(result) 
                }
            }, err => {
                result.push({
                    status: 'rejected',
                    reason: err
                })
                if (result.length === promiseArr.length) {resolve(result) 
                }
            })
        })  
    })   
}

Promise.any

Promise.any 的规定是这样:

  • 空数组或者所有 Promise 都是 rejected,则返回状态是 rejected 的新 Promsie,且值为 AggregateError 的谬误;
  • 只有有一个是 fulfilled 状态的,则返回第一个是 fulfilled 的新实例;
  • 其余状况都会返回一个 pending 的新实例;
Promise.any = function(promiseArr) {
    let index = 0
    return new Promise((resolve, reject) => {if (promiseArr.length === 0) return 
        promiseArr.forEach((p, i) => {Promise.resolve(p).then(val => {resolve(val)

            }, err => {
                index++
                if (index === promiseArr.length) {reject(new AggregateError('All promises were rejected'))
                }
            })
        })
    })
}

JS 闭包,你理解多少?

应该有面试官问过你:

  1. 什么是闭包?
  2. 闭包有哪些理论使用场景?
  3. 闭包是如何产生的?
  4. 闭包产生的变量如何被回收?

这些问题其实都能够被看作是同一个问题,那就是面试官在问你:你对 JS 闭包理解多少?

来总结一下我听到过的答案,尽量齐全还原候选人面试的时候说的原话。

答案 1: 就是一个 function 外面 return 了一个子函数,子函数拜访了里面那个函数的变量。

答案 2: for 循环外面能够用闭包来解决问题。

for(var i = 0; i < 10; i++){setTimeout(()=>console.log(i),0)
}
// 控制台输入 10 遍 10.
for(var i = 0; i < 10; i++){(function(a){setTimeout(()=>console.log(a),0)
    })(i)
}
 // 控制台输入 0 -9

答案 3: 以后作用域产产生了对父作用域的援用。

答案 4: 不晓得。是跟浏览器的垃圾回收机制无关吗?

开杠了。请问,小伙伴的答案和以上的内容有多少类似水平?

其实,拿着这些问题好好想想,你就会发现这些问题都只是为了最终那一个问题。

闭包的底层实现原理

1. JS 执行上下文

咱们都晓得,咱们手写的 js 代码是要通过浏览器 V8 进行预编译后能力真正的被执行。例如变量晋升、函数晋升。举个栗子。

// 栗子:var d = 'abc';
function a(){console.log("函数 a");
};
console.log(a);   // ƒ a(){ console.log("函数 a"); }
a();              // '函数 a'
var a = "变量 a";  
console.log(a);   // '变量 a'
a();              // a is not a function
var c = 123;

// 输入后果及程序:// ƒ a(){ console.log("函数 a"); }
// '函数 a'
// '变量 a'
// a is not a function

// 栗子预编后相当于:function a(){console.log("函数 a");
};
var d;
console.log(a);  // ƒ a(){ console.log("函数 a"); }
a();              // '函数 a'

a = "变量 a";     // 此时变量 a 赋值,函数申明被笼罩

console.log(a); // "变量 a"
a();         // a is not a function

那么问题来了。请问是谁来执行预编译操作的?那这个谁又是在哪里进行预编译的?

是的,你的纳闷没有错。js 代码运行须要一个运行环境,那这个环境就是 执行上下文。是的,js 运行前的预编译也是在这个环境中进行。

js 执行上下文分三种:

  • 全局执行上下文:代码开始执行时首先进入的环境。
  • 函数执行上下文:函数调用时,会开始执行函数中的代码。
  • eval 执行上下文:不倡议应用,可疏忽。

那么,执行上下文的周期,分为两个阶段:

  • 创立阶段

    • 创立词法环境
    • 生成变量对象 (VO),建设 作用域链 作用域链 作用域链(重要的事说三遍)
    • 确认 this 指向,并绑定this
  • 执行阶段。这个阶段进行变量赋值,函数援用及执行代码。

你当初猜猜看,预编译是产生在什么时候?

噢,我遗记说了,其实与编译还有另一个称说:执行期上下文

预编译产生在函数执行之前。预编译四部曲为:

  1. 创立 AO 对象
  2. 找形参和变量申明,将变量和形参作为 AO 属性名,值为undefined
  3. 将实参和形参相对立
  4. 在函数体里找到函数申明,值赋予函数体。最初程序输入变量值的时候,就是从 AO 对象中拿。

所以,预编译真正的后果是:

var AO = {a = function a(){console.log("函数 a");};
    d = 'abc'
}

咱们从新来。

1. 什么叫变量对象?

变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建设的一个对象,用来寄存以后执行环境中的变量。

2. 变量对象 (VO) 的创立过程

变量对象的创立,是在执行上下文创立阶段,顺次通过以下三个过程:

  • 创立 arguments 对象。

    对于函数执行环境,首先查问是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入 arguments 对象中。否则,将参数名和 undefined 组成的键值对放入 arguments 对象中。

// 举个栗子 
function bar(a, b, c) {console.log(arguments);  // [1, 2]
    console.log(arguments[2]); // undefined
}
bar(1,2)
  • 当遇到同名的函数时,前面的会笼罩后面的。
console.log(a); // function a() {console.log('Is a ?') }
function a() {console.log('Is a');
}
function a() {console.log('Is a ?')
}

/**ps: 在执行第一行代码之前,函数申明曾经创立实现. 前面的对之前的申明进行了笼罩。**/
  • 查看以后环境中的变量申明并赋值为 undefined。当遇到同名的函数申明, 为了防止函数被赋值为 undefined , 会疏忽此申明
console.log(a); // function a() {console.log('Is a ?') }
console.log(b); // undefined
function a() {console.log('Is a');
}
function a() {console.log('Is a ?');
}
var b = 'Is b';
var a = 10086;

/** 这段代码执行一下,你会发现 a 打印后果仍旧是一个函数,而 b 则是 undefined。**/

依据以上三个步骤,对于变量晋升也就晓得是怎么回事了。

3. 变量对象变为流动对象

执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数援用并执行其余代码,此时,变量对象变为流动对象。

咱们还是举下面的例子:

console.log(a); // function a() {console.log('fjdsfs') }
console.log(b); // undefined
function a() {console.log('Is a');
}
function a() {console.log('Is a?');
}
var b = 'Is b';
console.log(b); // 'Is b'
var a = 10086; 
console.log(a);  // 10086
var b = 'Is b?';
console.log(b); // 'Is b?'

在下面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:

// 创立过程
EC= {VO:{}; // 创立变量对象
  scopeChain: {}; // 作用域链}
VO = {argument: {...}; // 以后为全局上下文,所以这个属性值是空的
  a: <a reference> // 函数 a  的援用地址  b: undefiend  // 见上文创立变量对象的第三步}

词法作用域(Lexical scope

这里想阐明,咱们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript的一个简单之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就始终往上一级,直到它在全局执行上下文中查找为止。(如果最初找不到,它就是 undefined)。

再来举个栗子:

 1: let top = 0; // 
 2: function createWarp() {3:   function add(a, b) {
 4:     let ret = a + b
 5:     return ret
 6:   }
 7:   return add
 8: }
 9: let sum = createWarp()
10: let result = sum(top, 8)
11: console.log('result:',result)


剖析过程如下:

  • 在全局上下文中申明变量top 并赋值为 0.
  • 2 – 8 行。在全局执行上下文中申明了一个名为 createWarp 的变量,并为其调配了一个函数定义。其中第 3 - 7 行形容了其函数定义,并将函数定义存储到那个变量 (createWarp) 中。
  • 第 9 行。咱们在全局执行上下文中申明了一个名为 sum 的新变量,临时,值为 undefined
  • 第 9 行。遇到 (),表明须要执行或调用一个函数。 那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 显著,曾经在步骤 2 中创立结束。接着,调用它。
  • 调用函数时,回到第 2 行。创立一个新的 createWarp 执行上下文。咱们能够在 createWarp 的执行上下文中创立自有变量。js 引擎createWarp 的上下文增加到调用堆栈(call stack)。因为这个函数没有参数,间接跳到它的主体局部.
  • 3 – 6 行。咱们有一个新的函数申明,createWarp 执行上下文中创立一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。
  • 第 7 行,咱们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容形成该函数定义。
  • createWarp 调用结束,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义依然存在,因为它返回并赋值给了 sum 变量。(ps: 这才是闭包产生的变量存于内存当中的假相
  • 接下来就是简略的执行过程,不再赘述。。
  • ……
  • 代码执行结束,全局执行上下文被销毁。sum 和 result 也跟着被销毁。

小结一下

当初,如果再让你答复什么是闭包,你能答出多少?

其实,大家说的都对。不论是函数返回一个函数,还是产生了内部作用域的援用,都是有情理的。

所以,什么是闭包?

  • 解释一下作用域链是如何产生的。
  • 解释一下 js 执行上下文的创立、执行过程。
  • 解释一下闭包所产生的变量放在哪了。
  • 最初请把以上 3 点联合起来说给面试官听。

说下对 JS 的理解吧

是基于原型的动静语言,次要独特个性有 this、原型和原型链。

JS 严格意义上来说分为:语言规范局部(ECMAScript)+ 宿主环境局部

语言规范局部

2015 年公布 ES6,引入诸多新个性使得可能编写大型项目变成可能,规范自 2015 之后以年号代号,每年一更

宿主环境局部

  • 在浏览器宿主环境包含 DOM + BOM 等
  • 在 Node,宿主环境包含一些文件、数据库、网络、与操作系统的交互等

代码输入后果

const promise = new Promise((resolve, reject) => {console.log(1);
  console.log(2);
});
promise.then(() => {console.log(3);
});
console.log(4);

输入后果如下:

1 
2 
4

promise.then 是微工作,它会在所有的宏工作执行完之后才会执行,同时须要 promise 外部的状态发生变化,因为这里外部没有发生变化,始终处于 pending 状态,所以不输入 3。

正文完
 0