Skip to content

JS 相关面试题第一版 ​

作者:HearLing
更新于:17 小时前
字数统计:7.8k 字
阅读时长:29 分钟

总结了关于 js 常见的面试题,由于涉及到的问题太多了,有个别的问题例如事件循环,如果是完全没基础的话,需要看网络专栏里的事件循环。

当然也是由于是第一版,还有一些问题没有解答的很深,也有可能存在错误,如果你有发现错误,或者想要补充问题或答案的,都可以在本站的 GitHub 上

。

数据类型 ​

8 种基础数据类型 ​

  • 7 种原始数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6 新增,表示独一无二的值)和 BigInt(es10 新增)

  • 1 种引用数据类型——Object

按储存形式分:

  • 原始数据类型:直接存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据

  • 引用数据类型:同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储指针,指向堆中该实体的地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

数据类型的判断 ​

typeOf ​

  • 原始数据类型:除了 null 都可以显示正确的类型

  • 引用数据类型(Object):除了函数都会显示 object

javascript
console.log(typeof 2) // number
console.log(typeof true) // boolean
console.log(typeof 'str') // string
console.log(typeof []) // object     []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function () {}) // function
console.log(typeof {}) // object
console.log(typeof undefined) // undefined
console.log(typeof null) // object     null 的数据类型被 typeof 解释为 object

instanceof ​

  • 能正确判断对象类型,不能精准判断原始数据类型
  • 原理:通过判断对象的原型链中是不是能找到类型的 prototype
javascript
console.log(2 instanceof Number) // false
console.log(true instanceof Boolean) // false
console.log('str' instanceof String) // false
console.log(Array.isArray([])) // true
console.log(typeof function () {} === 'function') // true
console.log({} instanceof Object) // true

constructor ​

  • 通过判断 constructor 确定数据类型
  • 不可靠在于,创建对象更改了原型
javascript
console.log((2).constructor === Number) // true
console.log(true.constructor === Boolean) // true
console.log('str'.constructor === String) // true
console.log([].constructor === Array) // true
console.log(function () {}.constructor === Function) // true
console.log({}.constructor === Object) // true

function Fn() {}

Fn.prototype = []

const f = new Fn()

console.log(f.constructor === Fn) // false
console.log(f.constructor === Array) // true

Object.prototype.toString.call() ​

  • toString()调用时,会获取 this 指向的那个对象的[[Class]]属性的值(使用了 call)。
  • 使用 Object 对象的原型方法 toString ,使用 call 进行狸猫换太子,借用 Object 的 toString 方法
javascript
const a = Object.prototype.toString

console.log(a.call(2)) // [object Number]
console.log(a.call(true)) // [object Boolean]
console.log(a.call('str')) // [object String]
console.log(a.call([])) // [object Array]
console.log(a.call(() => {})) // [object Function]
console.log(a.call({})) // [object Object]
console.log(a.call(undefined)) // [object Undefined]
console.log(a.call(null)) // [object Null]

精度问题 0.1+0.2!==0.3 ​

在进制转换和进阶运算的过程中出现精度损失,JavaScript 使用 Number 类型表示数字(整数和浮点数),使用 64 位表示一个数字。

  • 进制转换

    0.1 和 0.2 转换成二进制后会无限循环:

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)

由于 IEEE 754 尾数位数限制,需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。

  • 对阶运算

将两个进行运算的浮点数的阶码对齐的操作,目的是为使两个浮点数的尾数能够进行加减运算。运算结果是:

0.0100110011001100110011001100110011001100110011001100

转换成十进制之后就是:0.30000000000000004

null 和 undefined 的区别? ​

一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。

对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等 号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

JavaScript 执行机制/执行上下文 ​

变量提升 ​

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

至于为什么要变量提升,在于 JavaScript 代码在执行前需要先编译,编译时变量和函数会被放到变量环境中。

模拟演示一下:

javascript
// 这是我们正常写的代码:
showName()
console.log(myName)
var myName = 'HearLing'
function showName() {
  console.log('showName')
}

模拟变量提升后的效果:

javascript
const name = undefined
function showName() {
  console.log('showName')
}

showName()
console.log(myName)
myName = 'HearLing'

函数和变量在执行前都提升到代码开头。而对于出现了同名的变量或者函数,最终生效的是最后一个(覆盖)。

块级作用域 ​

  • 作用域:是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期
  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁

块级作用域:就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

ES6 引入了 let 和 const 关键字,从而使 JavaScript 拥有了块级作用域,能够解决一些变量提升带来的问题。

使用 let 关键字声明的变量是可以被改变的,而使用 const 声明的变量其值是不可以被改变的。

以下是块级作用域的理解考察题:

javascript
function foo() {
  const a = 1
  const b = 2
  {
    const b = 3
    var c = 4
    const d = 5
    console.log(a)
    console.log(b)
  }
  console.log(b)
  console.log(c)
  console.log(d)
}
foo()

分析一下:

    1. 变量环境:a = undefined; c = undefined;词法环境:b = undefined;
    1. 变量环境:a = 1; c = undefined;词法环境:作用域块 1(或者叫 foo 函数执行上下文):b = 2;作用域块 2:b = undefined;d = undefined;
    1. 变量环境:a = 1; c = 4;词法环境:作用域块 1:b = 2;作用域块 2:b = 3;d = 5:
    1. 先在词法环境找,再在变量环境找(作用域链),于是依次输出:1、3、2、4、err

作用域链、闭包 ​

通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念。理解作用域链是理解闭包的基础。

解释 ​

作用域链:是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。

闭包:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

例题 ​

javascript
function foo() {
  let myName = 'HearLing'
  const test1 = 1
  const test2 = 2
  const innerBar = {
    getName() {
      console.log(test1)
      return myName
    },
    setName(newName) {
      myName = newName
    },
  }
  return innerBar
}
const bar = foo()
bar.setName('hl')
bar.getName()
console.log(bar.getName())

根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 myName 和 test1。

使用场景 ​

  • 在函数外部能够访问到函数内部的变量。
  • 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

this 指向相关面试题 ​

全局 this 定为指向 window(一般面试是在游览器环境讨论)。

在下面的 默认绑定、隐式绑定、显示绑定等,我们来看看 this 的指向是什么样的~

默认绑定 ​

函数调用时,没有前缀直接调用的情况,指向全局对象 window fn();

隐式绑定 ​

前面存在调用它的对象,这个 this 就会隐式绑定到这个对象 obj.fn() this 指向 obj

隐式丢失: ​

var o = obj.fn() ; o(); this 指向全局进行了一个赋值了,那么这个 o 其实是走的默认绑定 this 指向全局 window

显示绑定 ​

call、bind、apply 都是用来显示改变 this 指向 call、bind、apply 他们有什么区别: bind 返回函数, call 和 apply 他们传参的区别:apply 传数组。

call、apply、bind 的手写都要会,详情见手写题。

箭头函数的绑定 ​

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则 this 的值则被设置为全局对象。

重点:

  • 创建箭头函数时,就已经确定了它的 this 指向。
  • 箭头函数内的 this 指向外层的 this。

new 的绑定 ​

new 也要会手写,详情见手写题 new。当使用 new 关键字调用函数时,函数中的 this 一定是 JS 创建的新对象。

this 的优先级 ​

显示>隐式>默认

new>隐式>默认

不好记住的话,你可以便利的记成:箭头函数、new、bind、apply 和 call、obj.、直接调用、不在函数里。

8 例题 ​

javascript
const name = 'window'

const person1 = {
  name: 'person1',
  show1() {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3() {
    return function () {
      console.log(this.name)
    }
  },
  show4() {
    return () => console.log(this.name)
  },
}
const person2 = { name: 'person2' }

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()

// 正确答案如下:

person1.show1() // person1,隐式绑定,this指向调用者 person1
person1.show1.call(person2) // person2,显式绑定,this指向 person2

person1.show2() // window,箭头函数绑定,this指向外层作用域,即全局作用域
person1.show2.call(person2) // window,箭头函数绑定,this指向外层作用域,即全局作用域

person1.show3()() // window,默认绑定,这是一个高阶函数,调用者是window
// 类似于`var func = person1.show3()` 执行`func()`
person1.show3().call(person2) // person2,显式绑定,this指向 person2
person1.show3.call(person2)() // window,默认绑定,调用者是window

person1.show4()() // person1,箭头函数绑定,this指向外层作用域,即person1函数作用域
person1.show4().call(person2) // person1,箭头函数绑定,
// this指向外层作用域,即person1函数作用域
person1.show4.call(person2)() // person2

原型与原型链 ​

  • 当我们使用构造函数新建一个对象后,在这个对象的内部 将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。
  • 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又 会有自己的原型,于是就这样一直找下去,也就是原型链的概念。

原型链考察输出题 ​

javascript
function Fn() {
  this.x = 100
  this.y = 200
  this.getX = function () {
    console.log(this.x)
  }
}
Fn.prototype = {
  y: 400,
  getX() {
    console.log(this.x)
  },
  getY() {
    console.log(this.y)
  },
  sum() {
    console.log(this.x + this.y)
  },
}
const f1 = new Fn()
const f2 = new Fn()
console.log(f1.getX === f2.getX) // false
console.log(f1.getY === f2.getY) // true
console.log(f1.__proto__.getY === Fn.prototype.getY) // true
console.log(f1.__proto__.getX === f2.getX) // false
console.log(f1.getX === Fn.prototype.getx)
console.log(f1.constructor) // [Function:Object]
console.log(Fn.prototype.__proto__.constructor) // [Function:Object]
f1.getX() // 100
f1.__proto__.getX() // undefined
f2.getY() // 200
f2.__proto__.getY() // 400
f1.sum() // 300
Fn.prototype.sum() // undedined+400=NAN

实现继承的几种方式 ​

实现继承的几种方式,主要有以下几种,其中重点为寄生组合继承。

原型继承 ​

优点:

  • 方法复用
  • 由于方法定义在父类的原型上,复用了父类构造函数原型上的方法。

缺点:

  • 创建的子类实例不能传参。
  • 子类实例共享了父类构造函数的引用属性(如:arr)。
javascript
const person = {
  stu: ['x', 'y', 'z'],
}

const p1 = Object.create(person)
p1.stu.push('A')

console.log(person.stu) // ['x','y','z','A']

组合式继承 ​

优点:

● 可传参: 子类实例创建可以传递参数。 ● 方法复用: 同时所有子类可以复用父类的方法。 ● 不共享父类引用属性: 子类的改变不会引起父类引用类型的共享。

缺点:

● 组合继承调用了两次父类的构造函数,造成了不必要的消耗。

javascript
function Father(name) {
  this.name = name
  this.type = ['x', 'y', 'z']
}

Father.prototype.sayName = function () {
  console.log(this.name)
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

Son.prototype = new Father()
Son.prototype.constructor = Son

// 优点一:可传参
const son1 = new Son('aaa', 11)
const son2 = new Son('bbb', 12)

// 优点二:共享父类方法
son1.sayName()
son2.sayName()

// 优点三:不共享父类引用类型
son1.type.push('Q')

console.log(son1.type)
console.log(son2.type)

寄生组合继承 ​

核心思想: 组合继承 + 原型继承结合两者的优点。 优点: 完美! 缺点:无!

javascript
function Father(name) {
  this.name = name
  this.type = ['x', 'y', 'z']
}
Father.prototype.sayName = function () {
  console.log(this.name)
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son

const son1 = new Son('kk', 18)

son1.sayName()
son1.type.push('Q')
console.log(son1.type)

js 用几种方式实现继承(构造函数继承、原型链继承、组合方式继承) ​

  • 构造函数继承:通过构造函数来实现的继承,只能继承父类构造函数的属性,如果原型 prototype 上面还有方法甚至原型链上的方法,不会继承。
  • 借助原型链实现继承:当我们修改某一个对象时,该函数所产出的所有新实例都会发生改变,这就造成了 数据污染 问题,肯定不是我们想要的。(因为它们引用的是同一个父类实例对象)
  • 组合方式实现继承:拿得是父类的原型对象,依旧没有自己的 constructor。(和父类的原型对象是同一个对象,导致 constructor 也指向父类)
  • ES6 的 extend 继承:ES6 的 extend 继承其实就是寄生组合式继承的语法糖。

Promise、async/await、setTimeout ​

此小结主要来总结,异步场景比较常见的一些面试题。

关于一些手写题,如手写 Promise、手写异步调度器等等这些都归纳在经典手写题里了,这里就大部分说理论~

Promise 是什么 ​

Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象。

关键点:

  • 1、Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)
  • 2、Promise 构造函数接收一个函数作为参数,该函数的两个参数分别是 resolve 和 reject
  • 3、一个 promise 对象只能改变一次状态,成功或者失败后都会返回结果数据。
  • 4、then 方法可以接收两个回调函数作为参数,第一个回调函数是 Promise 对象的状态改变为 resoved 是调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。其中第二个参数可以省略。
  • 5、catch 方法,该方法相当于最近的 then 方法的第二个参数,指向 reject 的回调函数,另一个作用是,在执行 resolve 回调函数时,如果出错,抛出异常,不会停止陨星,而是进入 catch 方法中。

除了 then 块以外,其它两种块能否多次使用? ​

可以,finally 与 then 一样会按顺序执行,但是 catch 块只会执行第一个,除非 catch 块里有异常。所以最好只安排一个 catch 和 finally 块。

then 块如何中断? ​

then 块默认会向下顺序执行,return 是不能中断的,可以通过 throw 来跳转至 catch 实现中断。

什么是 async/await ​

async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。

关键点:

  • 1、async 确保了函数返回一个 promise,也会将非 promise 的值包装进去。
  • 2、await 的关键词,它只在 async 函数内工作,语法是:let value = await promise;,让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
  • 3、如果有 error,就会抛出异常 —— 就像那里调用了 throw error 一样。否则,就返回结果。

经典手写题 ​

以下是 js 考察中经常会被面试到的一些面试题及答案的总结,供参考:

call ​

  • 语法:fn.call(obj,...args)
  • 功能:执行 fn,使 this 为 obj,并将后面的 n 个参数传给 fn
javascript
Function.prototype.myCall = function (obj, ...args) {
  if (obj == undefined || obj == null) {
    obj = globalThis
  }
  obj.fn = this
  const res = obj.fn(...args)
  delete obj.fn
  return res
}
value = 2

const foo = {
  value: 1,
}

function bar(name, age) {
  console.log(name, age, this.value)
}

bar.myCall(foo, 'HearLing', 18) // HearLing 18 1
bar.myCall(null, 'HearLing', 18) // HearLing 18 2

apply ​

  • 语法:fn.apply(obj,arr)
  • 功能:执行 fn,使 this 为 obj,并 arr 数组中元素传给 fn
javascript
Function.prototype.myAplly = function (obj, arr) {
  if (obj == undefined || obj == null) {
    obj = globalThis
  }
  obj.fn = this
  const res = obj.fn(...arr)
  delete obj.fn
  return res
}
value = 2

const foo = {
  value: 1,
}

function bar(name, age) {
  console.log(name, age, this.value)
}

bar.myAplly(foo, ['HearLing', 18]) // HearLing 18 1
bar.myAplly(null, ['HearLing', 18]) // HearLing 18 2

bind ​

  • 语法:fn.bind(obj,...args)
  • 功能:返回一个新函数,给 fn 绑定 this 为 obj,并制定参数为后面的 n 个参数
javascript
Function.prototype.myBind = function (obj, ...args) {
  const that = this
  const fn = function () {
    if (this instanceof fn) {
      return new that(...args)
    }
    else {
      return that.call(obj, ...args)
    }
  }
  return fn
}

value = 2

const foo = {
  value: 1,
}

function bar(name, age) {
  console.log(name, age, this.value)
}
const fn = bar.myBind(foo, 'HearLing', 18)
// fn() //HearLing 18 1
const a = new fn() // HearLing 18 undefined
console.log(a.__proto__) // bar {}

区别 call()/apply()/bind() ​

call(obj)/apply(obj)::调用函数, 指定函数中的 this 为第一个参数的值

bind(obj): 返回一个新的函数, 新函数内部会调用原来的函数, 且 this 为 bind()指定的第一参数的值

节流 ​

  • 理解:在函数多次频繁触发时,函数执行一次后,只有大于设定的执行周期后才会执行第二次
  • 场景:页面滚动(scroll)、DOM 元素的拖拽(mousemove)、抢购点击(click)、播放事件算进度信息
  • 功能:节流函数在设置的 delay 毫秒内最多执行一次(简单点说就是,我上个锁,不管你点了多少下,时间到了我才解锁)
javascript
function throttle(fn, delay) {
  let flag = true
  return (...args) => {
    if (!flag)
      return
    flag = false
    setTimeout(() => {
      fn.apply(this, args)
      flag = true
    }, delay)
  }
}

防抖 ​

  • 理解:在函数频繁触发是,在规定之间以内,只让最后一次生效
  • 场景:搜索框实时联想(keyup/input)、按钮点击太快,多次请求(登录、发短信)、窗口调整(resize)
  • 功能:防抖函数在被调用后,延迟 delay 毫秒后调用,没到 delay 时间,你又点了,清空计时器重新计时,直到真的等了 delay 这么多秒。
javascript
function debounce(fn, delay) {
  let timer = null
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

节流与防抖的区别 ​

首先概念上的不同,解释一下什么是防抖节流;然后就是使用场景的不同;经典区分图:

函数柯里化 curry ​

javascript
function mycurry(fn, beforeRoundArg = []) {
  return function () {
    const args = [...beforeRoundArg, ...arguments]
    if (args.length < fn.length) {
      return mycurry.call(this, fn, args)
    }
    else {
      return fn.apply(this, args)
    }
  }
}

function sum(a, b, c) {
  return a + b + c
}

const sumFn = mycurry(sum)
console.log(sumFn(1)(2)(3)) // 6

new ​

javascript
function newInstance(Fn, ...args) {
  const obj = {}
  obj.__proto__ = Fn.prototype
  const result = Fn.call(obj, ...args)
  // 如果Fn返回的是一个对象类型, 那返回的就不再是obj, 而是Fn返回的对象否则返回obj
  return result instanceof Object ? result : obj
}

instanceof ​

javascript
function instance_of(left, right) {
  const prototype = right.prototype
  while (true) {
    if (left === null) {
      return false
    }
    else if (left.__proto__ === prototype) {
      return true
    }
    left = left.__proto__
  }
}
const a = {}
console.log(instance_of(a, Object)) // true

深拷贝 ​

javascript
// 浅拷贝的方法
// JSON.parse(JSON.stringify(obj))
function deepClone(target, hashMap = new WeakMap()) {
  if (typeof target !== 'object' || target == null) {
    if (typeof target === 'function')
      return target.call(this, ...arguments)
    return target
  }
  if (target instanceof Date)
    return new Date(target)
  if (target instanceof RegExp)
    return new RegExp(target)
  const res = new target.constructor()
  if (hashMap.get(target))
    return hashMap.get(target)
  hashMap.set(res, target)
  for (const key in target) {
    res[key] = deepClone(deepClone(target[key], hashMap))
  }
  return res
}

const a = {
  i: Infinity,
  s: '',
  bool: false,
  n: null,
  u: undefined,
  sym: Symbol(),
  obj: {
    i: Infinity,
    s: '',
    bool: false,
    n: null,
    u: undefined,
    sym: Symbol(),
  },
  array: [
    {
      nan: Number.NaN,
      i: Infinity,
      s: '',
      bool: false,
      n: null,
      u: undefined,
      sym: Symbol(),
    },
    123,
  ],
  fn() {
    return 'fn'
  },
  date: new Date(),
  re: /hi\d/gi,
}
const a2 = deepClone(a)
console.log(a2 !== a)
console.log(a2.i === a.i)
console.log(a2.s === a.s)
console.log(a2.bool === a.bool)
console.log(a2.n === a.n)
console.log(a2.u === a.u)
console.log(a2.sym === a.sym)
console.log(a2.obj !== a.obj)
console.log(a2.array !== a.array)
console.log(a2.array[0] !== a.array[0])
console.log(a2.array[0].i === a.array[0].i)
console.log(a2.array[0].s === a.array[0].s)
console.log(a2.array[0].bool === a.array[0].bool)
console.log(a2.array[0].n === a.array[0].n)
console.log(a2.array[0].u === a.array[0].u)
console.log(a2.array[0].sym === a.array[0].sym)
console.log(a2.array[1] === a.array[1])
console.log(a2.fn !== a.fn)
console.log(a2.date !== a.date)
console.log(a2.re !== a.re)
// 都要为 true

数组扁平化 ​

javascript
// 递归展开
function flattern1(arr) {
  const res = []
  arr.foreach((item) => {
    if (Array.isArray(item)) {
      res.push(...flattern1(item))
    }
    else {
      res.push(item)
    }
  })
  return res
}

数组去重 ​

javascript
function unique(arr) {
  const res = []
  const obj = {}
  arr.forEach((item) => {
    if (obj[item] === undefined) {
      obj[item] = true
      res.push(item)
    }
  })
  return res
}
// 其他方法
// Array.from(new Set(array))
// [...new Set(array)]

手写 reduce ​

javascript
// 语法 array.reduce(function(prev, currentValue, currentIndex, arr), initialValue)
Array.prototype.MyReduce = function (fn, initialValue) {
  // 浅拷贝数组
  const arr = Array.prototype.slice.call(this)
  // 注意: reduce() 对于空数组是不会执行回调函数的。
  if (!arr.length)
    return

  let res // res(是上面的prev)
  // 默认初始值
  res = initialValue || arr[0]
  // 遍历数组的每一个值
  for (let i = initialValue ? 0 : 1; i < arr.length; i++) {
    // 每一个值都会在该方法中被(加工处理),
    res = fn.call(null, res, arr[i], i, this)
  }
  // 最后的返回值
  return res
}

带并发的异步调度器 ​

这类题目有很多,核心考察就是 限制运行任务的数量。

为了能快速理解,我先讲一个通俗的例子:首先要限制数量,我们可以用一个栈,栈不能超过两格(假设限制数量为2),当放进去的两个任务,一个快一些先执行完,那么弹出该任务,接下一个,如此类推。。。

进阶:两个请求一直占着位置,没有请求回数据,因为它们没执行完成导致后面的请求也进不来,导致阻塞,怎么办呢。。。第一肯定是要判断阻塞,两个请求占的时间过久。第二记录这两个请求并清空栈,允许其他链接请求。最后根据场景,对数据进行处理,比如你需要对没请求的数据再重新请求,或者提示等。

为了展示更加直观,我选了最经典的一道面试题:

setTimeout 实现 ​

用 setTimeout 实现需要 注意 的是它是直接 add 的 time 和 val ,而不是返回 promise 的函数,所以可以在 add 里实现:

javascript
// 设计并发调度器, 最多允许两个任务运行
const scheduler = new Scheduler(2)
// 这里的timer有的会写1有的会直接写1000,需要灵活解题
scheduler.addTask(5, '1')
scheduler.addTask(3, '2')
scheduler.addTask(1, '3')
scheduler.addTask(2, '4')
scheduler.start()
// 输出:2314

思路:

  1. 用一个 count 记录并发的数量,用一个 taskList 数组保存任务。
  2. addTask 如名字,将任务一一存入 taskList 。
  3. 递归调用 start ,递归结束条件没有数据了,进入条件没有超过并发数。再通过 count 记录并发数量,从数组取出来一个 count++ ,执行完一个 count-- 。
javascript
class Scheduler {
  constructor(maxNum) {
    this.maxNum = maxNum
    this.count = 0
    this.taskList = []
  }

  addTask(time, val) {
    this.taskList.push([time, val])
  }

  start() {
    if (!this.taskList.length)
      return
    if (this.count < this.maxNum) {
      const [time, val] = this.taskList.shift()
      this.count++
      setTimeout(() => {
        console.log(val)
        this.count--
        this.start()
      }, time * 1000)
      this.start()
    }
  }
}

promise 实现 ​

用 promise 写的话,实例代码就应该是下面这样:

javascript
function timeout(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time)
  })
}

const scheduler = new Scheduler()

function addTask(time, order) {
  scheduler.add(() => timeout(time).then(() => console.log(order)))
}

addTask(5000, '1')
addTask(3000, '2')
addTask(1000, '3')
addTask(2000, '4')

需要注意的是使用 promise 实现的话也是离不开循环 .then 的,所以我们抽出一个函数来实现 then 的链式调用。

  1. 需要一个函数来实现 add 记录要执行的 promiseCreator ,还需要一个函数在执行的时候就去第一个就可以了。

  2. 要求只有一个 add 函数,所以我们需要在 add 里记录 promiseCreator 以及执行 run 。

  3. run 来触发异步函数的执行,这里的触发有两处,一处为 add 一个 promise 就 run ,另一个是自己执行完一个再 then 里执行 run ,当大于 max 时阻止继续 run 。

这里如果想不明白的话,可以换一个生活里的场景。比如吃火锅,我喜欢吃虾滑,虾滑一个个下锅,煮熟就把它放到碗里,可碗就那么大只能放两个虾滑,吃一个才能从锅里取一个,直到锅里没有虾滑了。相信有了上述的这个场景,你能写出不一样的题解,这是我实现的既符合题意又相对简洁的 promise 实现:

javascript
class Scheduler {
  constructor() {
    this.taskList = []
    this.maxNum = 2
    this.count = 0
  }

  add(promiseCreator) {
    this.taskList.push(promiseCreator)
    this.run()
  }

  run() {
    if (this.count >= this.maxNum || this.taskList.length == 0)
      return

    this.count++
    this.taskList
      .shift()()
      .then(() => {
        this.count--
        this.run()
      })
  }
}

async 实现 ​

最简单地写法还得是 async (这里换了一种写法,你也可以用类实现),然后帮助理解如果没有 start 函数,怎么直接在 add 函数中实现逻辑:

  1. 用一个 count 记录并发的数量,用一个 taskList 数组保存任务。
  2. 异步函数 add 接受异步任务返回 promise 。
  3. 这里没有递归调用, add 一个异步任务,就执行,并用 count 记录并发数量。
  4. 关键思想:当并发数超过限制,我们 await 一个不被 resolve 的 promise ,当完成了一个请求有位置了,才 resolve。
javascript
function scheduler(maxNum) {
  const taskList = []
  let count = 0

  return async function add(promiseCreator) {
    if (count >= maxNum) {
      await new Promise((resolve, reject) => {
        taskList.push(resolve)
      })
    }
    count++
    const res = await promiseCreator()
    count--
    if (taskList.length > 0)
      taskList.shift()()

    return res
  }
}

实现一个 EventMitter(设计模式) ​

在这个类里我们要明确,需要具备的变量和函数:

  1. 需要一个维护事件和监听者的对象
  2. 使用 on 函数注册事件监听者
  3. 使用 emit 函数发布事件
  4. 使用 off 函数移除某个事件的一个监听者
javascript
class EventEmitter {
  constructor() {
    // 维护事件及监听者
    this.listeners = {}
  }

  /**
   * 注册事件监听者
   * @param {string} type 事件类型
   * @param {Function} cb 回调函数
   */
  on(type, cb) {
    if (!this.listeners[type])
      this.listeners[type] = []

    this.listeners[type].push(cb)
  }

  /**
   * 发布事件
   * @param {string} type 事件类型
   * @param  {...any} args 参数列表,把emit传递的参数赋给回调函数
   */
  emit(type, ...args) {
    if (this.listeners[type]) {
      this.listeners[type].forEach((cb) => {
        cb(...args)
      })
    }
  }

  /**
   * 移除某个事件的一个监听者
   * @param {string} type 事件类型
   * @param {Function} cb 回调函数
   */
  off(type, cb) {
    if (this.listeners[type]) {
      const targetIndex = this.listeners[type].findIndex(item => item === cb)
      if (targetIndex !== -1)
        this.listeners[type].splice(targetIndex, 1)

      if (this.listeners[type].length === 0)
        delete this.listeners[type]
    }
  }
}
// 创建事件管理器实例
const ee = new EventEmitter()
// 注册事件监听者
ee.on(
  '设计模式',
  (fn1 = function (price) {
    console.log(`HearLing订阅设计模式这本书的价格是:${price}`)
  })
)
ee.on('你不知道JavaScript', (price) => {
  console.log(`HearLing订阅你不知道JavaScript这本书的价格是:${price}`)
})
ee.emit('设计模式', 100) // 输出HearLing订阅设计模式这本书的价格是:100

ee.off('设计模式', fn1)
ee.emit('设计模式') // 此时事件监听已经被移除,没有console.log

EventMitter 增强版 ​

另增加函数,完善功能:

  1. 使用 once 函数只执行一次
  2. 使用 offAll 函数移除某个事件的所有监听者
javascript
// 在上节代码新增
/**
 * 某个事件只监听一次
 * @param {String} type 事件类型
 * @param {Function} cb 回调函数
 */
once(type, cb) {
   const execFn = () => {
     cb.apply(this);
     this.off(type, execFn);
   };
   this.on(type, execFn);
}

/**
 * 移除某个事件的所有监听者
 * @param {String} type 事件类型
 */
offAll(type) {
    if (this.listeners[type]) {
        delete this.listeners[type]
    }
}

快排(算法) ​

  • 时间复杂度:nlogn~n2
  • 代码实现:
javascript
function quickSort(arr) {
  if (arr.length < 2)
    return arr

  const cur = arr[arr.length - 1]
  const left = arr.filter((v, i) => v <= cur && i !== arr.length - 1)
  const right = arr.filter(v => v > cur)
  return [...quickSort(left), cur, ...quickSort(right)]
}
// console.log(quickSort([3, 6, 2, 4, 1]));

大数相加(算法) ​

javascript
function add(a, b) {
  // 取两个数字的最大长度
  const maxLength = Math.max(a.length, b.length)
  // 用0去补齐长度
  a = a.padStart(maxLength, 0) // "0009007199254740991"
  b = b.padStart(maxLength, 0) // "1234567899999999999"
  // 定义加法过程中需要用到的变量
  let t = 0
  let f = 0 // "进位"
  let sum = ''
  for (let i = maxLength - 1; i >= 0; i--) {
    t = Number.parseInt(a[i]) + Number.parseInt(b[i]) + f
    f = Math.floor(t / 10)
    sum = (t % 10) + sum
  }
  if (f !== 0)
    sum = `${f}${sum}`

  return sum
}

js 常用方法(ES6 新特性) ​

常考的,比如简单列举,数组方法有哪些,以及 Set、Map、Proxy 等 ES6 方法

对于数组方法、字符串方法等还不熟悉的朋友,可以访问

,自己好好背和练习,这里就不一个个列举出来了。

Set: ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。关于 set 的使用:可以通过链式的使用 add 添加值,可以使用 has 方法检查 Set 实例中是否存在特定的值,可以使用 size 属性获得 Set 实例的长度,使用 clear 方法删除 Set 中的数据。

WeakSet: 与 Set 类似,也是不重复的值的集合。但是 WeakSet 的成员只能是对象,而不能是其他类型的值。WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。

Map:它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

WeakMap:与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

Proxy:Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写(一种中介者模式)。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

ES6 新特性,涉及的内容会比较多,暂时不准备专门出一篇讲,已经有总结得很好的网站了 https://es6.ruanyifeng.com/ ,如果还没掌握的强烈建议多看几遍咯。一般面试,其实就要你说一说你了解的 ES6 的哪些东西,然后面试官再挑其中的一两个问,所以也不用太纠结部分不记得~

Contributors ​

HearLing
文章作者:HearLing
文章链接:
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ChoDocs!