乐趣区

关于javascript:一年前端面试打怪升级之路

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 = 0
var 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,所以拜访任何属性都失去 35
var proxy = new Proxy({}, {get: function(target, property) {return 35;}
});

proxy.time // 35
proxy.name // 35
proxy.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/GoogleChro…

// commonJS require
const 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.js
if (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.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.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 = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.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.873046875ms
var 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.964111328125ms
console.time('innerHTML')
let html=''
for (let i = 0; i < times; i++) {html+='<div></div>'}
document.body.innerHTML += html // 31.919921875ms
console.timeEnd('innerHTML')

如何阻止事件冒泡

  • 一般浏览器应用:event.stopPropagation()
  • IE 浏览器应用:event.cancelBubble = true;

对节流与防抖的了解

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则从新计时。这能够应用在一些点击申请的事件上,防止因为用户的屡次点击向后端发送屡次申请。
  • 函数节流是指规定一个单位工夫,在这个单位工夫内,只能有一次触发事件的回调函数执行,如果在同一个单位工夫内某事件被触发屡次,只有一次能失效。节流能够应用在 scroll 函数的事件监听上,通过事件节流来升高事件调用的频率。

防抖函数的利用场景:

  • 按钮提交场景:防⽌屡次提交按钮,只执⾏最初提交的⼀次
  • 服务端验证场景:表单验证须要服务端配合,只执⾏⼀段间断的输⼊事件的最初⼀次,还有搜寻联想词性能相似⽣存环境请⽤ lodash.debounce

节流函数的适⽤场景:

  • 拖拽场景:固定工夫内只执⾏⼀次,防⽌超⾼频次触发地位变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:防止短时间内屡次触发动画引起性能问题
退出移动版