SHANKS

中级前端工程师

3,887 字
约 10 min
面试前端JavaScript

在JS中定义枚举的首选语法是什么

Object.freeze

Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

我们平常都是用const来定义对象枚举,但是const对应的对象的值还是可以改动的,所以Object.freeze更适合枚举。

const obj = {
  prop: 42
};

Object.freeze(obj);

obj.prop = 33;
// Throws an error in strict mode

console.log(obj.prop);
// expected output: 42

Object.freeze并没有递归冻结对象。

let obj1 = {
  internal: {
    a:123
  }
};

Object.freeze(obj1);
obj1.internal.a = 'aValue';

obj1.internal.a // 'aValue'

JS按位取反操作符~

预备知识

提示

计算机中没法做减法的,它的减法是通过加法来实现,需要加上一个负数,所以不得不引入一个符号位。

原码

提示

是最简单的机器数表示法。用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。

以带符号位的四位二进制值数为例:

提示

1010: 最高位为’1’,表示这是一个负数,其他三位’010’,即(0*2^2)+(1*2^1)+(0*2^0)=2(‘^’表示幂运算符)所以1010表示十进制数(-2)。

下图给出部份正负数数的二进制原码表示法 既然都有正负数了,那么我们开始运算:

提示

0001+0010=0011 (1+2=3) 0000+1000=1000 (+0+(-0)=-0) 0001+1001=1010 (1+(-1)=-2)出现问题

于是我们可以看到其实正数之间的加法通常是不会出错的,因为它就是一个很简单的二进制加法。

而正数与负数相加,或负数与负数相加,就要引起莫名其妙的结果,这都是该死的符号位引起的。0分为+0和-0也是因他而起。

所以原码,虽然直观易懂,易于正值转换。但用来实现加减法的话,运算规则总归是太复杂。于是反码来了。

反码

提示

我们知道,原码最大的问题就在于一个数加上他的相反数不等于零。

例如:0001+1001=1010 (1+(-1)=-2) 0010+1010=1100 (2+(-2)=-4)

提示

反码:正数的反码还是等于原码
负数的反码就是他的原码除符号位外,按位取反,例子:
3是正数,反码与原码相同,则可以表示为0011
-3的原码是1011,符号位保持不变,低三位(011)按位取反得(100)
所以-3的反码为1100

那我们再试下,用反码的方式解决一下原码的问题:

提示

0001+1110=1111 (1+(-1)= - 0)

互为相反数相加等于0,解决。虽然是得到的结果是1111也就是-0,所以还不是很精确

好,我们再试着做一下两个负数相加

提示

1110(-1)+ 1101(-2)= 1011(-4)

-1 + (-2) = -4 ? 为了解决以上问题,补码就登场了。

补码

补码定义

正数的补码等于他的原码

负数的补码等于反码+1

在《计算机组成原理中》,补码的另外一种算法 是

提示

负数的补码等于他的原码自低位向高位,尾数的第一个‘1’及其右边的‘0’保持不变,左边的各位按位取反,符号位不变。

那正数的补码呢?加上一个正数,加法器就直接可以实现。所以它的补码就还是它本身。

补码实例 现在来看几个例子:

负数相加

1111(-1)+1110(-2)=1101(-3)

正负数相加

1000(-8) +0011(3)=1011(-5)

1110 (-2) + 0011(3) = 0001(1)

~运算符

提示

作用于补码,将每一位二进制都取反:

~5: ~0101(5) => 1010(-6) ~-1: ~1111(-1) => 0000(0) ~0:~0000(0) => 1111(-1)

经常被用到indexOf,如果是没有找到返回-1时,可以用!~-1表示true

模拟实现一个完美的Promise

提示

这里说的是模拟,所以用setTimeout来模拟,但是setTimeout是属于宏任务,原生的Promise是微任务。但是可以在调用上面最大程度模拟出来,模拟之前可以看看PromiseA+规范,当我写完代码后,又画出了实现Promise的流程图。

Promise/A+规范

提示

  1. ‘promise’是一个对象或者函数
  2. ‘thenable’是一个对象或者函数
  3. ‘value’是promise状态成功时的值
  4. ‘reason’是promise状态失败时的值 下面是:promise的属性,当然还有一些静态方法没有给出,比如:Promise.resolvePromise.racePromise.all等等

要求

  1. 一个promise必须有3个状态,pending,fulfilled(resolved),rejected当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态。当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变。
  2. 一个promise必须有一个then方法,then方法接受两个参数:
    其中onFulfilled方法表示状态从pending——>fulfilled(resolved)时所执行的方法,而onRejected表示状态从pending——>rejected所执行的方法。
    promise.then(onFulfilled,onRejected)
  3. 为了实现链式调用,then方法必须返回一个新的Promise,并且将上一个promise的状态传入到新的promise上,以便后面的链式调用,可以在then返回一个一个promise,return new Promise(),所以我们新建了一个resolvePromise函数来处理这块逻辑。 下面使用ES6的class完整的模拟的Promise
class MyPromise {
  constructor(callback) {
    this.status = 'pending'
    this.value = undefined // status为resolved时返回的值
    this.reason = undefined // status为rejected时返回的值
    // 用来保存then传进来的函数,当状态改变时调用,声明成数组的原因是一个promise可能多处定义then或catch
    this.onFullfilledArray = []
    this.onRejectedArray = []
    try {
      // 由于resolve、reject都是在类外面执行的,所以需要绑定this
      callback(this.resolve.bind(this), this.reject.bind(this))
    } catch (error) {
      this.reject(error)
    }
  }

  /** 传入两个promise,将第二个的promise的value或者reason转移到currentPromise
 * @callback resolve
 * @callback reject
 * @param {Promise} currentPromise 当前promise对象
 * @param {*} x 当前resolve或者reject之后的结果
 * @param {resolve} resolve 当前promise对象的resolve
 * @param {reject} reject 当前promise对象的reject
 */
  static resolvePromise(currentPromise, x, resolve, reject) {
    if (currentPromise === x) {
      reject(new TypeError('Chaining cycle'));
    }
    // 如果结果x存在且是对象或者是一个函数
    if (x && typeof x === 'object' || typeof x === 'function') {
      let used; // then(cb1,cb2)cb1,cb2两者只能调用其中一个
      try {
        let then = x.then;
        // 如果返回值x有then函数,则
        if (typeof then === 'function') {
          then.call(x, (y) => {
            if (used) return;
            used = true;
            MyPromise.resolvePromise(currentPromise, y, resolve, reject);
          }, (r) => {
            if (used) return;
            used = true;
            reject(r);
          });
        } else {
          if (used) return;
          used = true;
          resolve(x);
        }
      } catch (e) {
        if (used) return;
        used = true;
        reject(e);
      }
    } else {
      resolve(x);
    }
  }

  reject(error) {
    if (this.status === 'pending') {
      this.status = 'rejected'
      this.reason = error
      this.onRejectedArray.forEach(f => {
        // 执行then传的函数
        f(error)
      })
    }
    // console.log('reject', error)
  }

  resolve(value) {
    if (this.status === 'pending') {
      this.status = 'resolved'
      this.value = value
      this.onFullfilledArray.forEach(f => {
        // 执行then传的函数
        f(value)
      })
    }
    // console.log('resolved', value)
  }

  // then函数可以传一个或两个函数
  then(onFullfilled, onRejected) {
    onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    let tempPromise,
      self = this
    switch (this.status) {
      case 'pending':
        tempPromise = new MyPromise((resolve, reject) => {
          // 保存.then传的第一个函数
          self.onFullfilledArray.push(function (value) {
            setTimeout(() => {
              try {
                let x = onFullfilled(value)
                MyPromise.resolvePromise(tempPromise, x, resolve, reject)
              } catch (error) {
                reject(error)
              }
            })
          })
          // 保存.then传的第二个函数
            self.onRejectedArray.push(function (reason) {
              setTimeout(() => {
                try {
                    let x = onRejected(reason)
                    MyPromise.resolvePromise(tempPromise, x, resolve, reject)
                } catch (error) {
                  reject(error)
                }
              })
            })
        })
        break;
      case 'resolved':
        tempPromise = new MyPromise((resolve, reject) => {
          setTimeout(() => {
            try {
                let x = onFullfilled(self.value)
                MyPromise.resolvePromise(tempPromise, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          })
        })
        break;
      case 'rejected':
        tempPromise = new MyPromise((resolve, reject) => {
          setTimeout(() => {
            try {
              if (onRejected) {
                let x = onRejected(self.reason)
                MyPromise.resolvePromise(tempPromise, x, resolve, reject)
              }
            } catch (error) {
              reject(error)
            }
          });
        })
        break;
    }
    return tempPromise
  }
  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

为了更好的理解这段代码,画出了一个流程图:

继承的几种方式

function Father(name) {
  // 属性
  this.name = name || 'father',
    // 实例方法
    this.sleep = function () {
      console.log(this.name + "正在睡觉");
    }
}
// 原型方法
Father.prototype.look = function (book) {
  console.log(this.name + "正在看:" + book);
}

原型继承

父类中私有的和公有的都继承到了子类原型上

function Son(){}
Son.prototype = new Father() // 重写了Son的原型
Son.prototype.constructor = Son

构造继承

只是将Father的私有属性拷贝一份给Son,并没有将原型链指向Father

function Son() {
  Father.call(this)
}

混合模式

有点弊端,会实例化两次Father,而且Father的私有属性也会在Son的原型上

function Son() {
  Father.call(this)
}
Son.prototype = new Father()
Son.prototype.constructor = Son

寄生组合模式

解决了混合模式的弊端

function Son() {
  Father.call(this)
}
// Object.create的Polyfill
function myCreate(proto) {
  function fn() {}
  fn.prototype = proto
  return fn
}
Son.prototype = myCreate(Father.prototype)
Son.constructor = Son

注意

如果将Son.prototype = myCreate(Father.prototype)改成Son.prototype = Father.prototype,虽然还是继承了,但是Father的原型和Son的原型是同一个引用地址,所以改动Son.prototype时就会将Father一起改动。

ES6中extends的实现

function Son() {
  Father.call(this)
}
Son.prototype = Object.create(Father.prototype,
  { constructor: { value: Son, writable: true, configurable: true } });
// Son.__proto__ = Father

js中new一个对象的过程

MDN原文

首先了解new做了什么,使用new关键字调用函数(new ClassA(…))的具体步骤:

  1. Creates a blank, plain JavaScript object;

  2. Links (sets the constructor of) this object to another object;

  3. Passes the newly created object from Step 1 as the this context;

  4. Returns this if the function doesn’t return its own object.

  5. 创建一个空的简单JavaScript对象(即{});

  6. 链接该对象(即设置该对象的构造函数)到另一个对象 ;

  7. 将步骤1新创建的对象作为this的上下文 ;

  8. 如果该函数没有返回对象,则返回this。

let a = {}
// a的构造器
a.constructor // ƒ Object() { [native code] }
let b = new a() // Uncaught TypeError: a is not a constructor

a的构造器是指向Object()函数,不是它自己的,所以不能实例化,所以我们只能这样let c = Object()

class也有自己的构造器(class其实就是function),所以也可以实例化,其他的都没办法实例化

实现new

function myNew(target, ...args) {
  const protoObj = Object.create(target.prototype)
  const result = target.apply(protoObj, args)
  return typeof result === 'object' ? result : protoObj
}

function A(name, age) {
  this.name = name || 'aa'
  this.age = age || 18
}

const a = myNew(A, 'cjh', 16)
console.log(a) // A { name: 'cjh', age: 16 }

箭头函数是否可以实例化

const fun1 = () => {}
function fun2() {}
console.log(fun1.prototype) // undefined
console.log(fun2.prototype) // {constructor: ƒ}

箭头函数没有prototype、没有自己的this指向、不可以使用arguments,而new的过程中有个操作是将__proto__指向prototype,所以不能用new实例化箭头函数

for与forEach的区别

提示

for循环每一次遍历都会重新检查跳出的条件,比如数组的长度,但是forEach只会保存第一次的数组长度。

let arr = [1,2,3]

arr.forEach(item => {
  if(item === 2) {
    arr.push(4)
  }
  console.log(item)
})
// 输出: 1 2 3

for (let i = 0; i < arr.length; i++) {
  if (arr[i] === 2){
    arr.push(4)
  }
  console.log(arr[i])
}
// 输出:1 2 3 4

装饰器的原理

分类

装饰器分为两类:作用于类的装饰器、作用于方法的装饰器,底层都是函数。

  • 作用于类的装饰器,写法举例
function log (target) {  // 默认传参为 被修饰的类
    target.prototype.logger = () => {console.log('装饰器--被调用')};
}

@log       // log 必须是函数
class Myclass {};

const test = new Myclass();
test.logger();          // 装饰器--被调用
  • 作用于方法的装饰器,写法举例
Object.defineProperty(obj, prop, descriptor);
class C {
   @readonly (true);
   method () {
      console.log('cat');
   }
}

function readonly (value) {
   // target: 类原型
   // key:    被修饰的属性或者方法
   // descriptor: 被修饰的属性或方法的描述符对象
   return function (target, key, descriptor) {
      descriptor.writable = !value;
      return descriptor;
   }
}

const c = new C();
c.method = () => {console.log('dog');} // 重写了method这个类方法
c.method() // cat  => 设置属性只读成功

eval&&Function

eval

eval可以取到上下文,但是在ES5后有个硬性规定:通过一个引用来调用它,而不是直接的调用 eval。 从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。这就意味着,例如,下面的代码的作用声明创建一个全局函数,并且 eval 中的这些代码在执行期间不能在被调用的作用域中访问局部变量

function test() {
  var x = 2, y = 4;
  console.log(eval('x + y'));  // 直接调用,使用本地作用域,结果是 6
  var geval = eval; // 等价于在全局作用域调用
  console.log(geval('x + y')); // 间接调用,使用全局作用域,throws ReferenceError 因为`x`未定义
  (0, eval)('x + y'); // 另一个间接调用的例子
​}

Function

与eval不同的是它可以入参,还有不同的就是作用域总是指向全局作用域。

MDN原文:

Functions created with the Function constructor do not create closures to their creation contexts; they always are created in the global scope. When running them, they will only be able to access their own local variables and global ones, not the ones from the scope in which the Function constructor was created. This is different from using eval with code for a function expression.

翻译:

使用函数构造函数创建的函数不会对其创建上下文创建闭包;它们总是在全局范围内创建的。在运行它们时,它们只能访问自己的局部变量和全局变量,而不能访问创建函数构造函数的作用域中的变量。这不同于对函数表达式的代码使用eval。

var x = 10;

function createFunction1() {
    var x = 20;
    return new Function('return x;'); // 这里的 x 指向最上面全局作用域内的 x
}

function createFunction2() {
    var x = 20;
    function f() {
        return x; // 这里的 x 指向上方本地作用域内的 x
    }
    return f;
}

var f1 = createFunction1();
console.log(f1());          // 10
var f2 = createFunction2();
console.log(f2());          // 20

Map && WeakMap

WeakMap存在的意义

WeakMap的专用场合就是防止内存泄漏,它的键所对应的对象,可能会在将来消失,WeakMap就会自动清除这个引用,而不是额外保留一份。

Map

让我们先理解Map的基本用法:

let obj = { test: 2 } // obj对象引用的计数是1
let map = new Map()
map.set(obj, '66') // obj对象引用的计数是2
console.log(obj) // { test: 2 }
console.log(map.get(obj))  // '66'
obj.one = 1
// 由于对应的是引用地址,所以还是可以取的到
console.log(obj) // { test: 2, one: 1 }
console.log(map.get(obj))  // '66'
obj = { test: 2, one: 1 }
console.log(map.get(obj))  // 'null'
obj = null // obj对象引用的计数是1
console.log(map) // Map { { test: 2, one: 1 } => '66' }

从上面的例子可以看出,不管obj里面的值怎么变,都可以拿到对应的值,除非是赋值然后引用地址变了,才取不到map的值。最后将obj置为nullobj对象引用的计数变为1,还是不会被垃圾回收机制回收。

WeakMap

let obj = { test: 2 } // obj对象引用的计数是1
let map = new WeakMap() // obj对象引用的计数是1
map.set(obj, '66')
console.log(map.get(obj))  // 'null'
obj = null // obj对象引用的计数是0 随时会被垃圾回收,其实map里面也没有了这个键值对
console.log(map) // WeakMap { [items unknown] }

WeakMap经常在保存节点时有用,请看下面例子:

const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"

上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

也就是说,上面的 DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

Page: DOMContentLoaded, load, beforeunload, unload

  • DOMContentLoaded:浏览器已经加载完HTML、dom tree已经构建完成,但是外部资源像<img>标签和样式表还没有被加载
  • load 不仅html已经被加载了,而且外部资源、图片、样式也被加载完
  • beforeunload/unload:用户离开页面时会发生的事件,beforeunload用于用户要离开:我们可以检查用户是否保存了更改并询问他们是否真的要离开,unload用于用户几乎离开了,但我们仍然可以启动一些操作,例如发送统计信息

V8执行一段JS代码的过程

提示

首先明白的是,机器是读不懂JS代码,机器只能理解特定的机器码,那如果要让JS的逻辑在机器运行起来,就必须将JS的代码翻译成机器码,然后让机器识别。JS属于解释新型语言,对于解释型的语言说,解释器会对源码做如下分析:

  • 通过词法分析和语法分析生成AST(抽象语法树)
  • 生成字节码
  1. 生成AST 生成AST分为两步—词法分析和语法分析

栅格化布局

利用flex可以实现

import && require

import

编译时调用,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。由于是在编译时调用,所以webpack可以通过import的特性来产生依赖图谱。

// dep.js
export let a = 1
setTimeout(() => a += 1, 500)

// app.js
import { a } from 'dep'
setTimeout(function () {
  console.log(a)
}, 1000)

import {a} from f的时候,他其实在你引用的地方和倒出的地方的a之间建立了连接,即它们是指向同一内存的,即便是原始数据类型,你修改模块中的指也会导致引用处的变化

require

运行时调用,输出的是一个值的拷贝(浅拷贝),一旦输出一个值,模块内部的变化就影响不到这个值,由于是浅拷贝所以改对象中的值还是会对应更改的

// lib.js
var counter = 3
const obj = {
  test: 1,
}
function incCounter() {
  counter++
  obj.test++
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
  obj,
}

// main.js
// main.js
var mod = require('./demo')

console.log(mod.counter) // 1
console.log(mod.obj.test) // 1
mod.incCounter()
console.log(mod.counter) // 1
console.log(mod.obj.test) // 2

多使用位运算提高代码效率

JS代码编译步骤

1: js代码 经历 词法分析,成为token 2: 然后经过语法分析,将token转换成AST(抽象语法树) 3: 用解释器根据AST,生成字节码 4: 使用JIT即时编译技术,将字节码转成机器码,然后计算机就可以识别执行了。

所以我们使用判断奇偶数的时候用8 & 18 % 2就有区别了,机器码其实就是二进制,所以8 & 1只需要运算1000 & 0001,而8 % 2中%需要转成机器码能识别的运算符,所以位运算就会快一点。

console.log是异步还是同步

我们先看下面得例子:

let a = {
  b : {
    c: 1
  }
}
console.log(a)
a.b.c = 2

咋一看好像是异步的,但是为什么数字就是同步的呢?其实里面有个坑,我们都知道对象是引用类型,所以console.log打印引用类型的时候是打印指针指向的引用地址的内容,我们手动展开的时候其实是调用了当前对象的get方法,这是才去读取改引用地址里面的内容,导致一些人说console.log是异步。

node环境下的console.log

node下的console.log

我们首先要知道Node.js中实现console.log的原理 function log() { process.stdout.write( util.format.apply(this, arguments); )} 等同于问process.stdout.write是同步还是异步的? 其实官方文档已经给出了答案,A note on process I/O process.stdout和process.stderr是根据系统环境来判定的同步还是异步的 Files: synchronous on Windows and Linux TTYs (Terminals): asynchronous on Windows, synchronous on Unix Pipes (and sockets): synchronous on Windows, asynchronous on Unix 那么console.log也是根据系统环境来判定同步还是异步的。

在请求帧动画中会经常操作dom节点,操作dom需要时间,怎么优化

如何在 Array.forEach 中正确使用 Async

const arr = [1, 2, 3]

const sleep = (delay, v) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, delay)
  })
}

arr.forEach(async (v, i) => {
  await sleep(1000 - (i * 100), i)
  console.log(v)
})
// 输出:
// 3
// 2
// 1

上面输出3、2、1,说明await是没有生效的,看了forEach的Polyfill就知道,是在一个while循环里面执行的,相当于上面是三个函数一起执行,我们想要的效果是第一个函数执行完再执行第二个函数,我们可以用reduce

const arr = [1, 2, 3]

const sleep = (delay, v) => {
 return new Promise((resolve, reject) => {
   setTimeout(() => {
     resolve(1)
   }, delay)
 })
}
arr.reduce(async (initial, v, i) => {
 // 因为此时的initial是上一个函数的返回值,如果没有promise没有执行完,会在await的关键字下停顿
 await initial
 await sleep(1000 - (i * 100), i)
 console.log(v)
}, 0)
// 输出:
// 1
// 2
// 3

我们可以看到reduce的Polyfill:

if (!Array.prototype.reduce) {
 Object.defineProperty(Array.prototype, 'reduce', {
   value: function (callback /*, initialValue */) {
     // ...more
     while (k < len) {
       if (k in o) {
         value = callback(value, o[k], k, o)
       }
       k++
     }
     return value
   }
 })
}

可以看到value就是initial,从value = callback(value, o[k], k, o)可以看出来initial保存这上一个函数的返回值,由于我们用了aysnc所以会返回一个promise对象,然后用await initial后可以在当前函数停顿。

Loading comments...