写点什么

一年前端面试打怪升级之路

作者:loveX001
  • 2022-11-21
    浙江
  • 本文字数:19003 字

    阅读完需:约 62 分钟

Promise 是什么?

Promise 是异步编程的一种解决方案:从语法上讲,promise 是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。promise 有三种状态: pending(等待态),fulfiled(成功态),rejected(失败态) ;状态一旦改变,就不会再变。创造 promise 实例后,它会立即执行。


const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";
function MyPromise(fn) { // 保存初始化状态 var self = this;
// 初始化状态 this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值 this.value = null;
// 用于保存 resolve 的回调函数 this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数 this.rejectedCallbacks = [];
// 状态转变为 resolved 方法 function resolve(value) { // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变 if (value instanceof MyPromise) { return value.then(resolve, reject); }
// 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变, if (self.state === PENDING) { // 修改状态 self.state = RESOLVED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.resolvedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 状态转变为 rejected 方法 function reject(value) { // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变 if (self.state === PENDING) { // 修改状态 self.state = REJECTED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.rejectedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 将两个方法传入函数执行 try { fn(resolve, reject); } catch (e) { // 遇到错误时,捕获错误,执行 reject 函数 reject(e); }}
MyPromise.prototype.then = function(onResolved, onRejected) { // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数 onResolved = typeof onResolved === "function" ? onResolved : function(value) { return value; };
onRejected = typeof onRejected === "function" ? onRejected : function(error) { throw error; };
// 如果是等待状态,则将函数加入对应列表中 if (this.state === PENDING) { this.resolvedCallbacks.push(onResolved); this.rejectedCallbacks.push(onRejected); }
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) { onResolved(this.value); }
if (this.state === REJECTED) { onRejected(this.value); }};
复制代码

了解 this 嘛,bind,call,apply 具体指什么

它们都是函数的方法


call: Array.prototype.call(this, args1, args2]) apply: Array.prototype.apply(this, [args1, args2]) :ES6 之前用来展开数组调用, foo.appy(null, []),ES6 之后使用 ... 操作符


  • New 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

  • 如果需要使用 bind 的柯里化和 apply 的数组解构,绑定到 null,尽可能使用 Object.create(null) 创建一个 DMZ 对象


四条规则:


  • 默认绑定,没有其他修饰(bind、apply、call),在非严格模式下定义指向全局对象,在严格模式下定义指向 undefined


function foo() {     console.log(this.a); }
var a = 2;foo();
复制代码


  • 隐式绑定:调用位置是否有上下文对象,或者是否被某个对象拥有或者包含,那么隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。而且,对象属性链只有上一层或者说最后一层在调用位置中起作用


function foo() {  console.log(this.a);}
var obj = { a: 2, foo: foo,}
obj.foo(); // 2
复制代码


  • 显示绑定:通过在函数上运行 call 和 apply ,来显示的绑定 this


function foo() {  console.log(this.a);}
var obj = { a: 2};
foo.call(obj);
复制代码


显示绑定之硬绑定


function foo(something) {  console.log(this.a, something);
return this.a + something;}
function bind(fn, obj) { return function() { return fn.apply(obj, arguments); };}
var obj = { a: 2}
var bar = bind(foo, obj);
复制代码


New 绑定,new 调用函数会创建一个全新的对象,并将这个对象绑定到函数调用的 this。


  • New 绑定时,如果是 new 一个硬绑定函数,那么会用 new 新建的对象替换这个硬绑定 this,


function foo(a) {  this.a = a;}
var bar = new foo(2);console.log(bar.a)
复制代码

协商缓存和强缓存的区别

(1)强缓存

使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。


强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。


(1)服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。


(2)Expires 是 http1.0 中的方式,因为它的一些缺点,在 HTTP 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。它有很多不同的值,


Cache-Control可设置的字段:


  • public:设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用 max-age=来精确控制;

  • private:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的 HTML,通常都要设置这个字段值,避免代理服务器(CDN)缓存;

  • no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源;

  • no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;

  • max-age=:设置缓存的最大有效期,单位为秒;

  • s-maxage=:优先级高于 max-age=,仅适用于共享缓存(CDN),优先级高于 max-age 或者 Expires 头;

  • max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制。


一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires。


no-cache 和 no-store 很容易混淆:


  • no-cache 是指先要和服务器确认是否有资源更新,在进行判断。也就是说没有强缓存,但是会有协商缓存;

  • no-store 是指不使用任何缓存,每次请求都直接从服务器获取资源。

(2)协商缓存

如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。


上面已经说到了,命中协商缓存的条件有两个:


  • max-age=xxx 过期了

  • 值为no-store


使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。


协商缓存也可以通过两种方式来设置,分别是 http 头信息中的 EtagLast-Modified 属性。


(1)服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。


(2)因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确。


当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。


总结:


强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。

防抖

**防抖(debounce)**:触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。类似王者荣耀的回城功能,你反复触发回城功能,那么只认最后一次,从最后一次触发开始计时。


核心思想:每次事件触发就清除原来的定时器,建立新的定时器。使用 apply 或 call 调用传入的函数。函数内部支持使用 this 和 event 对象;


应用:防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。


实现


function debounce(fn, delay) {    // 利用闭包的原理    let timer = null;    return function(...args){        if(timer) clearTimeout(timer);        timer = setTimeout(() => {            // 改变 this 指向为调用 debounce 所指的对象            fn.call(this, ...args);            // fn.apply(this, args);        }, delay);    }}
复制代码

前端储存的⽅式有哪些?

  • cookies: 在 HTML5 标准前本地储存的主要⽅式,优点是兼容性好,请求头⾃带 cookie⽅便,缺点是⼤⼩只有 4k,⾃动请求头加⼊cookie 浪费流量,每个 domain 限制 20 个 cookie,使⽤起来麻烦,需要⾃⾏封装;

  • localStorage:HTML5 加⼊的以键值对(Key-Value)为标准的⽅式,优点是操作⽅便,永久性储存(除⾮⼿动删除),⼤⼩为 5M,兼容 IE8+ ;

  • sessionStorage:与 localStorage 基本类似,区别是 sessionStorage 当⻚⾯关闭后会被清理,⽽且与 cookie、localStorage 不同,他不能在所有同源窗⼝中共享,是会话级别的储存⽅式;

  • Web SQL:2010 年被 W3C 废弃的本地数据库数据存储⽅案,但是主流浏览器(⽕狐除外)都已经有了相关的实现,web sql 类似于 SQLite,是真正意义上的关系型数据库,⽤sql 进⾏操作,当我们⽤JavaScript 时要进⾏转换,较为繁琐;

  • IndexedDB: 是被正式纳⼊HTML5 标准的数据库储存⽅案,它是 NoSQL 数据库,⽤键值对进⾏储存,可以进⾏快速读取操作,⾮常适合 web 场景,同时⽤JavaScript 进⾏操作会⾮常便。

什么是同源策略

跨域问题其实就是浏览器的同源策略造成的。


同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议端口号域名必须一致。


同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。


同源政策主要限制了三个方面:


  • 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。

  • 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。

  • 当前域下 ajax 无法发送跨域请求。


同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者 script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。


参考:前端进阶面试题详细解答

原型/原型链

__proto__和 prototype 关系__proto__constructor对象独有的。2️⃣prototype属性是函数独有的


在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们是不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对象的原型。


当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。


特点:JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与 之相关的对象也会继承这一改变


  • 原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 FirefoxChrome 中,每个JavaScript对象中都包含一个__proto__(非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。

  • 构造函数: 可以通过new来 新建一个对象 的函数。

  • 实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数。


Object为例,我们常用的Object便是一个构造函数,因此我们可以通过它构建实例。


// 实例const instance = new Object()
复制代码


则此时, 实例为instance, 构造函数为Object,我们知道,构造函数拥有一个prototype的属性指向原型,因此原型为:


// 原型const prototype = Object.prototype
复制代码


这里我们可以来看出三者的关系:


  • 实例.__proto__ === 原型

  • 原型.constructor === 构造函数

  • 构造函数.prototype === 原型


// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线// 例如: // const o = new Object()// o.constructor === Object   --> true// o.__proto__ = null;// o.constructor === Object   --> false实例.constructor === 构造函数
复制代码



原型链


原型链是由原型对象组成,每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型,__proto__ 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链


  • 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出undefined

  • 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。


js 获取原型的方法


  • p.proto

  • p.constructor.prototype

  • Object.getPrototypeOf(p)


总结



  • 每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

  • 每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]]是内部属性,我们并不能访问到,所以使用 _proto_来访问。

  • 对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。

介绍一下 Rollup

Rollup 是一款 ES Modules 打包器。它也可以将项目中散落的细小模块打包为整块代码,从而使得这些划分的模块可以更好地运行在浏览器环境或者 Node.js 环境。


Rollup 优势:


  • 输出结果更加扁平,执行效率更高;

  • 自动移除未引用代码;

  • 打包结果依然完全可读。


缺点


  • 加载非 ESM 的第三方模块比较复杂;

  • 因为模块最终都被打包到全局中,所以无法实现 HMR

  • 浏览器环境中,代码拆分功能必须使用 Require.js 这样的 AMD


  • 我们发现如果我们开发的是一个应用程序,需要大量引用第三方模块,同时还需要 HMR 提升开发体验,而且应用过大就必须要分包。那这些需求 Rollup 都无法满足。

  • 如果我们是开发一个 JavaScript 框架或者库,那这些优点就特别有必要,而缺点呢几乎也都可以忽略,所以在很多像 React 或者 Vue 之类的框架中都是使用的 Rollup 作为模块打包器,而并非 Webpack


总结一下Webpack 大而全,Rollup 小而美


在对它们的选择上,我的基本原则是:应用开发使用 Webpack,类库或者框架开发使用 Rollup


不过这并不是绝对的标准,只是经验法则。因为 Rollup 也可用于构建绝大多数应用程序,而 Webpack 同样也可以构建类库或者框架。


BFC

块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。


IE 下为 Layout,可通过 zoom:1 触发


触发条件:


  • 根元素

  • position: absolute/fixed

  • display: inline-block / table

  • float 元素

  • ovevflow !== visible


规则:


  • 属于同一个 BFC 的两个相邻 Box 垂直排列

  • 属于同一个 BFC 的两个相邻 Boxmargin 会发生重叠

  • BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box的左边相接触 (子元素 absolute 除外)

  • BFC 的区域不会与 float 的元素区域重叠

  • 计算 BFC 的高度时,浮动子元素也参与计算

  • 文字层不会被浮动层覆盖,环绕于周围


应用:


  • 阻止margin重叠

  • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)

  • 自适应两栏布局

  • 可以阻止元素被浮动元素覆盖

async/await

Generator 函数的语法糖。有更好的语义、更好的适用性、返回值是 Promise


  • await 和 promise 一样,更多的是考笔试题,当然偶尔也会问到和 promise 的一些区别。

  • await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性,此时更应该使用 Promise.all。

  • 一个函数如果加上 async ,那么该函数就会返回一个 Promise


  • async => *

  • await => yield


// 基本用法
async function timeout (ms) { await new Promise((resolve) => { setTimeout(resolve, ms) })}async function asyncConsole (value, ms) { await timeout(ms) console.log(value)}asyncConsole('hello async and await', 1000)
复制代码


下面来看一个使用 await 的代码。


var a = 0var b = async () => {  a = a + await 10  console.log('2', a) // -> '2' 10  a = (await 10) + a  console.log('3', a) // -> '3' 20}b()a++console.log('1', a) // -> '1' 1
复制代码


  • 首先函数b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来

  • 因为 await 是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)

  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10

  • 然后后面就是常规执行代码了


优缺点:


async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。


async 原理


async/await语法糖就是使用Generator函数+自动执行器来运作的


// 定义了一个promise,用来模拟异步请求,作用是传入参数++function getNum(num){    return new Promise((resolve, reject) => {        setTimeout(() => {            resolve(num+1)        }, 1000)    })}
//自动执行器,如果一个Generator函数没有执行完,则递归调用function asyncFun(func){ var gen = func();
function next(data){ var result = gen.next(data); if (result.done) return result.value; result.value.then(function(data){ next(data); }); }
next();}
// 所需要执行的Generator函数,内部的数据在执行完成一步的promise之后,再调用下一步var func = function* (){ var f1 = yield getNum(1); var f2 = yield getNum(f1); console.log(f2) ;};asyncFun(func);
复制代码


  • 在执行的过程中,判断一个函数的promise是否完成,如果已经完成,将结果传入下一个函数,继续重复此步骤

  • 每一个 next() 方法返回值的 value 属性为一个 Promise 对象,所以我们为其添加 then 方法, 在 then 方法里面接着运行 next 方法挪移遍历器指针,直到 Generator函数运行完成


Proxy 代理

proxy 在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截


var proxy = new Proxy(target, handler);
复制代码


new Proxy()表示生成一个 Proxy 实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为


var target = {   name: 'poetries' }; var logHandler = {   get: function(target, key) {     console.log(`${key} 被读取`);     return target[key];   },   set: function(target, key, value) {     console.log(`${key} 被设置为 ${value}`);     target[key] = value;   } } var targetWithLog = new Proxy(target, logHandler);
targetWithLog.name; // 控制台输出:name 被读取 targetWithLog.name = 'others'; // 控制台输出:name 被设置为 others
console.log(target.name); // 控制台输出: others
复制代码


  • targetWithLog 读取属性的值时,实际上执行的是 logHandler.get :在控制台输出信息,并且读取被代理对象 target 的属性。

  • targetWithLog 设置属性值时,实际上执行的是 logHandler.set :在控制台输出信息,并且设置被代理对象 target 的属性的值


// 由于拦截函数总是返回35,所以访问任何属性都得到35var proxy = new Proxy({}, {  get: function(target, property) {    return 35;  }});
proxy.time // 35proxy.name // 35proxy.title // 35
复制代码


Proxy 实例也可以作为其他对象的原型对象


var proxy = new Proxy({}, {  get: function(target, property) {    return 35;  }});
let obj = Object.create(proxy);obj.time // 35
复制代码


proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截


Proxy 的作用


对于代理模式 Proxy 的作用主要体现在三个方面


  • 拦截和监视外部对对象的访问

  • 降低函数或类的复杂度

  • 在复杂操作前对操作进行校验或对所需资源进行管理


Proxy 所能代理的范围--handler


实际上 handler 本身就是 ES6 所新设计的一个对象.它的作用就是用来 自定义代理对象的各种可代理操作 。它本身一共有 13 中方法,每种方法都可以代理一种操作.其 13 种方法如下


// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。handler.getPrototypeOf()
// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。handler.setPrototypeOf()

// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。handler.isExtensible()

// 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。handler.preventExtensions()
// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。handler.getOwnPropertyDescriptor()

// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。andler.defineProperty()

// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。handler.has()
// 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。handler.get()

// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。handler.set()
// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。handler.deleteProperty()
// 在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。handler.ownKeys()
// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。handler.apply()

// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。handler.construct()
复制代码


为何 Proxy 不能被 Polyfill


  • 如 class 可以用function模拟;promise可以用callback模拟

  • 但是 proxy 不能用Object.defineProperty模拟


目前谷歌的 polyfill 只能实现部分的功能,如 get、set https://github.com/GoogleChrome/proxy-polyfill


// commonJS requireconst proxyPolyfill = require('proxy-polyfill/src/proxy')();
// Your environment may also support transparent rewriting of commonJS to ES6:import ProxyPolyfillBuilder from 'proxy-polyfill/src/proxy';const proxyPolyfill = ProxyPolyfillBuilder();
// Then use...const myProxy = new proxyPolyfill(...);
复制代码

Promise.resolve

Promise.resolve = function(value) {    // 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象    if(value instanceof Promise) return value;    // 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法    if(typeof value === "object" && 'then' in value) {        return new Promise((resolve, reject) => {           value.then(resolve, reject);        });    }    // 3.否则返回一个新的 Promise 对象,状态为 fulfilled    return new Promise(resolve => resolve(value));}
复制代码

对 Service Worker 的理解

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。


Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:


// index.jsif (navigator.serviceWorker) {  navigator.serviceWorker    .register('sw.js')    .then(function(registration) {      console.log('service worker 注册成功')    })    .catch(function(err) {      console.log('servcie worker 注册失败')    })}// sw.js// 监听 `install` 事件,回调中缓存所需文件self.addEventListener('install', e => {  e.waitUntil(    caches.open('my-cache').then(function(cache) {      return cache.addAll(['./index.html', './index.js'])    })  )})// 拦截所有请求事件// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据self.addEventListener('fetch', e => {  e.respondWith(    caches.match(e.request).then(function(response) {      if (response) {        return response      }      console.log('fetch source')    })  )})
复制代码


打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了: 在 Cache 中也可以发现所需的文件已被缓存:

类数组转化为数组的方法

题目描述:类数组拥有 length 属性 可以使用下标来访问元素 但是不能使用数组的方法 如何把类数组转化为数组?


实现代码如下:


const arrayLike=document.querySelectorAll('div')
// 1.扩展运算符[...arrayLike]// 2.Array.fromArray.from(arrayLike)// 3.Array.prototype.sliceArray.prototype.slice.call(arrayLike)// 4.Array.applyArray.apply(null, arrayLike)// 5.Array.prototype.concatArray.prototype.concat.apply([], arrayLike)

复制代码

深浅拷贝


1. 浅拷贝的原理和实现


自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象


方法一:object.assign


object.assign是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。


object.assign 的语法为:Object.assign(target, ...sources)
复制代码


object.assign 的示例代码如下:


let target = {};let source = { a: { b: 1 } };Object.assign(target, source);console.log(target); // { a: { b: 1 } };
复制代码


但是使用 object.assign 方法有几点需要注意


  • 它不会拷贝对象的继承属性;

  • 它不会拷贝对象的不可枚举的属性;

  • 可以拷贝 Symbol 类型的属性。


let obj1 = { a:{ b:1 }, sym:Symbol(1)}; Object.defineProperty(obj1, 'innumerable' ,{    value:'不可枚举属性',    enumerable:false});let obj2 = {};Object.assign(obj2,obj1)obj1.a.b = 2;console.log('obj1',obj1);console.log('obj2',obj2);
复制代码



从上面的样例代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能


方法二:扩展运算符方式


  • 我们也可以利用 JS 的扩展运算符,在构造对象的同时完成浅拷贝的功能。

  • 扩展运算符的语法为:let cloneObj = { ...obj };


/* 对象的拷贝 */let obj = {a:1,b:{c:1}}let obj2 = {...obj}obj.a = 2console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}obj.b.c = 2console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}/* 数组的拷贝 */let arr = [1, 2, 3];let newArr = [...arr]; //跟arr.slice()是一样的效果
复制代码


扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便


方法三:concat 拷贝数组


数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。


let arr = [1, 2, 3];let newArr = arr.concat();newArr[1] = 100;console.log(arr);  // [ 1, 2, 3 ]console.log(newArr); // [ 1, 100, 3 ]
复制代码


方法四:slice 拷贝数组


slice 方法也比较有局限性,因为它仅仅针对数组类型slice方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。


slice 的语法为:arr.slice(begin, end);
复制代码


let arr = [1, 2, {val: 4}];let newArr = arr.slice();newArr[2].val = 1000;console.log(arr);  //[ 1, 2, { val: 1000 } ]
复制代码


从上面的代码中可以看出,这就是浅拷贝的限制所在了——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝


手工实现一个浅拷贝


根据以上对浅拷贝的理解,如果让你自己实现一个浅拷贝,大致的思路分为两点:


  • 对基础类型做一个最基本的一个拷贝;

  • 对引用类型开辟一个新的存储,并且拷贝一层对象属性。


const shallowClone = (target) => {  if (typeof target === 'object' && target !== null) {    const cloneTarget = Array.isArray(target) ? []: {};    for (let prop in target) {      if (target.hasOwnProperty(prop)) {          cloneTarget[prop] = target[prop];      }    }    return cloneTarget;  } else {    return target;  }}
复制代码


利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了


2. 深拷贝的原理和实现


浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。


这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下


将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。


方法一:乞丐版(JSON.stringify)


JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将 JSON 字符串生成一个新的对象


let a = {    age: 1,    jobs: {        first: 'FE'    }}let b = JSON.parse(JSON.stringify(a))a.jobs.first = 'native'console.log(b.jobs.first) // FE
复制代码


但是该方法也是有局限性的


  • 会忽略 undefined

  • 会忽略 symbol

  • 不能序列化函数

  • 无法拷贝不可枚举的属性

  • 无法拷贝对象的原型链

  • 拷贝 RegExp 引用类型会变成空对象

  • 拷贝 Date 引用类型会变成字符串

  • 对象中含有 NaNInfinity 以及 -InfinityJSON 序列化的结果会变成 null

  • 不能解决循环引用的对象,即对象成环 (obj[key] = obj)。


function Obj() {   this.func = function () { alert(1) };   this.obj = {a:1};  this.arr = [1,2,3];  this.und = undefined;   this.reg = /123/;   this.date = new Date(0);   this.NaN = NaN;  this.infinity = Infinity;  this.sym = Symbol(1);} let obj1 = new Obj();Object.defineProperty(obj1,'innumerable',{   enumerable:false,  value:'innumerable'});console.log('obj1',obj1);let str = JSON.stringify(obj1);let obj2 = JSON.parse(str);console.log('obj2',obj2);
复制代码



使用 JSON.stringify 方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify 暂时还是无法满足的,那么就需要下面的几种方法了


方法二:基础版(手写递归实现)


下面是一个实现 deepClone 函数封装的例子,通过 for in 遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制


let obj1 = {  a:{    b:1  }}function deepClone(obj) {   let cloneObj = {}  for(let key in obj) {                 //遍历    if(typeof obj[key] ==='object') {       cloneObj[key] = deepClone(obj[key])  //是对象就再次调用该函数递归    } else {      cloneObj[key] = obj[key]  //基本类型的话直接复制值    }  }  return cloneObj}let obj2 = deepClone(obj1);obj1.a.b = 2;console.log(obj2);   //  {a:{b:1}}
复制代码


虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify 一样,还是有一些问题没有完全解决,例如:


  • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;

  • 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;

  • 对象的属性里面成环,即循环引用没有解决


这种基础版本的写法也比较简单,可以应对大部分的应用情况。但是你在面试的过程中,如果只能写出这样的一个有缺陷的深拷贝方法,有可能不会通过。


所以为了“拯救”这些缺陷,下面我带你一起看看改进的版本,以便于你可以在面试种呈现出更好的深拷贝方法,赢得面试官的青睐。


方法三:改进版(改进后递归实现)


针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。


  • 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;

  • 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;

  • 利用 ObjectgetOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object.create 方法创建一个新对象,并继承传入原对象的原型链;

  • 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 MapweakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值


如果你在考虑到循环引用的问题之后,还能用 WeakMap 来很好地解决,并且向面试官解释这样做的目的,那么你所展示的代码,以及你对问题思考的全面性,在面试官眼中应该算是合格的了


实现深拷贝


const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) { if (obj.constructor === Date) { return new Date(obj) // 日期对象直接返回一个新的日期对象 }
if (obj.constructor === RegExp){ return new RegExp(obj) //正则对象直接返回一个新的正则对象 }
//如果循环引用了就用 weakMap 来解决 if (hash.has(obj)) { return hash.get(obj) } let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
// 把cloneObj原型复制到obj上 hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) { cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key] } return cloneObj}
复制代码


// 下面是验证代码let obj = {  num: 0,  str: '',  boolean: true,  unf: undefined,  nul: null,  obj: { name: '我是一个对象', id: 1 },  arr: [0, 1, 2],  func: function () { console.log('我是一个函数') },  date: new Date(0),  reg: new RegExp('/我是一个正则/ig'),  [Symbol('1')]: 1,};Object.defineProperty(obj, 'innumerable', {  enumerable: false, value: '不可枚举属性' });obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))obj.loop = obj    // 设置loop成循环引用的属性let cloneObj = deepClone(obj)cloneObj.arr.push(4)console.log('obj', obj)console.log('cloneObj', cloneObj)
复制代码


我们看一下结果,cloneObjobj 的基础上进行了一次深拷贝,cloneObj 里的 arr 数组进行了修改,并未影响到 obj.arr 的变化,如下图所示


数字证书是什么?

现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。


首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。


这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。

Object.assign()

描述Object.assign()方法用于将所有可枚举Object.propertyIsEnumerable() 返回 true)和自有Object.hasOwnProperty() 返回 true)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。


实现


Object.assign = function(target, ...source) {    if(target == null) {        throw new TypeError('Cannot convert undefined or null to object');    }    let res = Object(target);    source.forEach(function(obj) {        if(obj != null) {            // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)            // hasOwnProperty 方法只考虑对象自身的属性            for(let key in obj) {                if(obj.hasOwnProperty(key)) {                    res[key] = obj[key];                }            }        }    });    return res;}
复制代码

如何高效操作 DOM

1. 为什么说 DOM 操作耗时


1.1 线程切换


  • 浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况,增加了另外一个机制,这两个引擎具有互斥性,也就是说在某个时刻只有一个引擎在运行,另一个引擎会被阻塞。操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息并读取下一个线程的状态信息,俗称上下文切换。而这个操作相对而言是比较耗时的

  • 每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作,然后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗。单次切换消耗的时间是非常少的,但是如果频繁地大量切换,那么就会产生性能问题


比如下面的测试代码,循环读取一百万次 DOM 中的 body 元素的耗时是读取 JSON 对象耗时的 10 倍。


// 测试次数:一百万次const times = 1000000// 缓存body元素console.time('object')let body = document.body// 循环赋值对象作为对照参考for(let i=0;i<times;i++) {  let tmp = body}console.timeEnd('object')// object: 1.77197265625ms
console.time('dom')// 循环读取body元素引发线程切换for(let i=0;i<times;i++) { let tmp = document.body}console.timeEnd('dom')// dom: 18.302001953125ms
复制代码


1.2 重新渲染


另一个更加耗时的因素是元素及样式变化引起的再次渲染,在渲染过程中最耗时的两个步骤为重排(Reflow)与重绘(Repaint)


浏览器在渲染页面时会将 HTML 和 CSS 分别解析成 DOM 树和 CSSOM 树,然后合并进行排布,再绘制成我们可见的页面。如果在操作 DOM 时涉及到元素、样式的修改,就会引起渲染引擎重新计算样式生成 CSSOM 树,同时还有可能触发对元素的重新排布和重新绘制


  • 可能会影响到其他元素排布的操作就会引起重排,继而引发重绘

  • 修改元素边距、大小

  • 添加、删除元素

  • 改变窗口大小

  • 引起重绘

  • 设置背景图片

  • 修改字体颜色

  • 改变 visibility属性值


了解更多关于重绘和重排的样式属性,可以参看这个网址:https://csstriggers.com/ (opens new window)


2. 如何高效操作 DOM


明白了 DOM 操作耗时之后,要提升性能就变得很简单了,反其道而行之,减少这些操作即可


2.1 在循环外操作元素


比如下面两段测试代码对比了读取 1000 次 JSON 对象以及访问 1000 次 body 元素的耗时差异,相差一个数量级


const times = 10000;console.time('switch')for (let i = 0; i < times; i++) {  document.body === 1 ? console.log(1) : void 0;}console.timeEnd('switch') // 1.873046875msvar body = JSON.stringify(document.body)console.time('batch')for (let i = 0; i < times; i++) {  body === 1 ? console.log(1) : void 0;}console.timeEnd('batch') // 0.846923828125ms
复制代码


2.2 批量操作元素


比如说要创建 1 万个 div 元素,在循环中直接创建再添加到父元素上耗时会非常多。如果采用字符串拼接的形式,先将 1 万个 div 元素的 html 字符串拼接成一个完整字符串,然后赋值给 body 元素的 innerHTML 属性就可以明显减少耗时


const times = 10000;console.time('createElement')for (let i = 0; i < times; i++) {  const div = document.createElement('div')  document.body.appendChild(div)}console.timeEnd('createElement')// 54.964111328125msconsole.time('innerHTML')let html=''for (let i = 0; i < times; i++) {  html+='<div></div>'}document.body.innerHTML += html // 31.919921875msconsole.timeEnd('innerHTML')
复制代码

如何阻止事件冒泡

  • 普通浏览器使用:event.stopPropagation()

  • IE 浏览器使用:event.cancelBubble = true;

对节流与防抖的理解

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。


防抖函数的应用场景:


  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次

  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce


节流函数的适⽤场景:


  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动

  • 缩放场景:监控浏览器 resize

  • 动画场景:避免短时间内多次触发动画引起性能问题


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
一年前端面试打怪升级之路_JavaScript_loveX001_InfoQ写作社区