拿去,JS 答不出个7788别来找我

222 阅读16分钟

1. 数据类型

引用类型,基本类型,有什么区别,堆栈,bigInt

2. 原型/原型链 🔥

  1. 对象的属性和方法继承:每个对象都有一个原型(prototype),通过原型链,一个对象可以继承其原型对象的属性和方法。__proto__的指向,当你访问一个对象的属性或方法时,如果当前的对象没有,会沿着原型链往上找,直到原型链的顶端(null
  2. 对象的方法重用:通过原型链,多个对象可以共享同一个原型对象的方法。这意味着我们可以在原型对象上定义一次方法,然后所有继承自该原型的对象都可以使用这个方法,避免了每个对象都拥有一份相同的方法的重复内存消耗
  3. 对象的多层继承:原型链可以形成多层继承关系,一个对象可以继承自另一个对象,而后者又可以继承自另一个对象,以此类推。这样的继承关系可以实现复杂的对象结构,帮助我们组织和管理代码
  4. 扩展对象的功能:通过在原型对象上添加新的属性和方法,可以实现对已有对象的功能扩展。由于原型链的特性,这些扩展会自动应用到所有继承自该原型的对象上,从而实现了动态的对象功能扩展

3. 作用域

  1. 局部、全局、作用域链(里 => 外)
  2. 词法作用域:静态的作用域,书写代码时就确定了,能够预测在执行代码的过程中如何查找标识符

4. 闭包 🔥

内部函数访问并记住外部函数的作用域的桥梁,创建函数的时候同时闭包就生成了

  1. 创建私有变量:定义模块私有变量,将操作函数暴露给外部,处理细节隐藏在模块内部
  2. 回调函数:实现抽象(数组方法中的各种回调函数, forEachmap等
    // 其他语言的abstract: 父类不知道要做什么的方法,交给子类实现
     if (typeof callback === 'function') {
         callback(div);
     }
    
  3. 延长变量的生命周期(本应该在外部函数执行完被垃圾回收的变量,因为内部函数的引用还在,不会被回收)
    1. 解决定时器延时打印(var存在变量提升,闭包可以创建一个临时变量记住当前的i)

      for (var i = 1; i <= 10; i++) {
          (function () {
              var j = i;
              setTimeout(function () {
                  console.log(j);
              }, 1000);
          })();
      }
      
      // 或者这样写
      for (var i = 1; i <= 10; i++) {
          (function (j) {
              setTimeout(function () {
                  console.log(j);
              }, 1000);
          })(i);
      }
      
      // 扩展:直接把var改成let也可以(块级作用域)
      
    2. 函数柯里化:多元参数转成一元

    3. 用 ES5 实现迭代器(见下 ES6+ 新特性 Symbol.iterator

5. 同步 & 异步

基于事件循环机制

  1. 背景:JS是单线程语言,多线程怕DOM操作冲突,但是如果有些事情一直卡住也不好,因此有了“异步”
  2. 事件循环:一个宏任务 => 所有微任务 => RequestAnimationFrame(重绘前) => 渲染 => requestIdleCallback => 下一个任务 🔥
    1. 宏任务: script 代码、setTimeoutsetInterval、I/O 操作等
    2. 微任务: Promise 的回调函数(thencatchfinally)、MutationObserverqueueMicrotask
    3. Promise里面的代码也是同步代码,它被resolve之后then等才会被推入微任务队列;await后面的代码会被推入微任务
    async function async1() {
       console.log('async1 start')
       await async2()
       console.log('async1 end')
    }
    
    async function async2() {
       console.log('async2')
    }
    
    console.log('script start')
    
    setTimeout(function () {
       console.log('setTimeout0')
    }, 0)
    
    setTimeout(function () {
       console.log('setTimeout2')
    }, 300)
    
    async1();
    
    new Promise(function (resolve) {
       console.log('promise1')
       resolve();
       console.log('promise2')
    }).then(function () {
       console.log('promise3')
    })
    
    console.log('script end')
    
    
    // script start
    // async1 start       async1执行的时候打印的
    // async2              async1执行的时候打印的, await后面的推入微任务
    // promise1
    // promise2             resolve后才推微任务
    // script end
    // async1 end
    // promise3
    // setTimeout0
    // setTimeout2          300ms后才进的宏任务,最后
    
    1. 扩展:浏览器的事件循环和node的事件循环有什么区别?【todo, orz】
  3. 异步的解决方案
    1. 回调函数(定时器回调、node 的各种事件比如 readFile 等)
    2. Promise
    3. Generate
    4. async/await
    • promise 和 async/await 是专门用于处理异步操作的
    • Generator 并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署 Interator 接囗...)
    • promise 编写代码相比 Generator、async 更为复杂化,且可读性也稍差
    • Generator、async需要与 promise 对象搭配处理异步情况
    • async 实质是 Generator 和 Promise 的语法糖,相当于会自动执行 Generator 函数
    • async 使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程目前的最终方案

6. ES6+ 新特性

  1. letconstvar(变量提升、暂时性死区、块级作用域;是否可重复赋值)🔥

  2. 扩展运算符 ...

    1. 浅拷贝
    2. 用于数组的解构赋值或者rest参数时, 只能放在最后
  3. 解构赋值

  4. Array 构造函数新增方法:from、of、find、finIndex、fill、entries、keys、values、includes、flat、flatMap、copyWithin、sort 等

  5. 函数的新增扩展

    1. 参数:默认值、rest参数(这俩不会计入函数的length属性)
    2. 新增 name 属性
    3. 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式
    4. 箭头函数
      1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
      2. 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误
      3. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
      4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
  6. 对象的新增扩展

    1. 属性简写
    2. 属性名表达式
    let lastWord = 'last word';
    
    const a = { 
    'first word': 'hello', 
    [lastWord]: 'world' 
    }; 
    
    a['first word'] // "hello" 
    a[lastWord] // "world" 
    a['last word'] // "world"
    
    1. super 关键字:指向当前对象的原型对象

    2. 属性的遍历

      1. for...in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)

      2. Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol 属性)的键名

      1. Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名
      2. Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名
      3. Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举

        上述遍历,都遵守同样的属性遍历的次序规则:

        1. 遍历所有数值键,按照数值升序排列
        2. 遍历所有字符串键,按照加入时间升序排列
        3. 最后遍历所有 Symbol 键,按照加入时间升序排
    3. 对象新增方法:is、assign(浅拷贝)、getOwnPropertyDescriptors(自身属性)、setPrototypeOf、getPrototypeOf、keys、values、entries、fromEntries

    4. 扩展:for...infor...of 的区别 🔥

      1. 遍历的对象类型:
      • in:用于遍历对象的可枚举属性,包括对象自身的属性以及从原型链继承的属性
      • of:用于遍历可迭代对象(实现了迭代器接口的对象),例如数组、字符串、MapSet
      1. 遍历顺序:
      • in:如上(但是取决于 JavaScript 引擎实现的细节。在大多数情况下是上面所说的情况)
      • of:根据迭代器接口的定义,按照可迭代对象中元素的顺序进行遍历。
      1. 遍历的内容:
      • in:属性名(字符串类型)
      • of:属性值
      1. 使用场景:
      • in:适用于遍历对象的属性,例如遍历对象的键或进行对象属性的操作
      • of:适用于遍历可迭代对象中的元素,例如遍历数组、字符串、MapSet等
      1. 再扩展:怎么使用迭代器接口自定义可迭代对象?
      const myIterable = {
       data: [1, 2, 3],
       // 用[Symbol.iterator]返回一个迭代器对象
       [Symbol.iterator]() {
         let index = 0;
         return {
           // next:逐个访问元素,done表示迭代是否结束
           next: () => {
             if (index < this.data.length) {
               return {
                 value: this.data[index++],
                 done: false
               };
             } else {
               return { value: undefined, done: true };
             }
           }
         }
       }
      } 
      
      for (const item of myIterable) {
       console.log(item);//1,2,3
      }
      
  7. Promise🔥

    1. 作用:
      1. 解决回调地域问题,链式调用代替嵌套调用
      2. 降低代码编写难度,可读性增强
      3. 更方便的错误处理机制,reject.catch
    2. async / await 的联系:
      1. async / await是基于 Promise 的语法糖,用同步的方式来编写异步代码。async 函数返回一个Promise对象(可以通过 thencatch 方法来处理 async 函数的执行结果),可以在函数内部使用 await 关键字来等待一个Promise对象的解析或拒绝
      2. async 函数可以使用 try / catch 块来捕获和处理异步操作中的错误,类似于Promisecatch 方法
      3. await 后面要是跟的不是 Promise, 会被自动包装成一个已解析的 Promise 对象
      async function main() {
          let result = await 42
          // 相当于 let result = await wrapValue(42);
          console.log(result);// 42
      }
      
        async function wrapValue(value) {
          return new Promise((resolve)=>{
              setTimeout(() => resolve(value),1000);
           });
       }
      
    3. 三个状态:pending、fullfilled、rejected
    4. 构造函数的实例方法:then、catch、finally
    5. 构造函数 Promise 的方法:all、race、allSettled、any、resolve、reject、try
    // 简单手写实现
    
    // Promise.all
    const promiseAll = (promiseArr) => {
        let resArr = [];
    
        return new Promise((resolve, reject) => {
            // 挨个执行一遍
            for(let p of promiseArr) {
                p.then((res) => {   
                    // 成功的结果push到结果数组里
                    resArr.push(res)
                }).catch((err) => {
                    // 一旦有失败的,立马结束
                    console.log('err:', err)
                    reject(err)
                }).finally(() => {
                    if(resArr.length === promiseArr.length) {
                        // 说明都执行完了
                        resolve(resArr)
                    }
                })
            }
        })
    }
    
    // Promise.race
    const promiseRace = (promiseArr) => {
        return new Promise((resolve, reject) => {
            for(let p of promiseArr) {
                p.then((res) => {  
                    // 哪个先返回结果就是哪个,then
                    resolve(res),
                    (err) => {
                        reject(err)
                    }
                })
            }
        })
    }
    
    // Promise.allSettled
    const promiseAllSettled = (promiseArr) => {
        let resArr = [];
    
        return new Promise((resolve) => {
             for(let p of promiseArr) {
                p.then((res) => {   
                    resArr.push({status: 'fulfilled', value: res})
                }).catch((err) => {
                    resArr.push({status: 'failed', reason: err})
                }).finally(() => {
                    if(resArr.length === promiseArr.length) {
                        // 说明都执行完了
                        resolve(resArr)
                    }
                })
            }
        })
    }
    
    // 测试
    (function () {
        const testArr = [Promise.resolve(1),Promise.reject('error')]
        promiseAllSettled(testArr)
            .then((res)=>{console.log('res:', res)})
            .catch((err)=>{console.log('err:', err)})
    })();
    
  8. 模块化 module

    1. 作用:代码的抽象、封装、复用、管理
    2. JS的模块化机制:commonJS、AMD(require.js)、CMD(sea.js)
    3. commonJS 和 es module(import)的区别 🔥
      1. 语法差异
      • CommonJS:使用 require() 函数来导入模块,使用 module.exportsexports 来导出模块
      • ES6 模块:使用 import 关键字来导入模块,使用 export 关键字来导出模块
      1. 运行时加载 vs 静态加载
      • CommonJS:模块在运行时加载,即在代码执行到 require() 时,才会加载所需的模块
      • ES6 模块:模块在编译时静态加载,即在代码解析阶段就会生成模块依赖关系,使得模块的导入和导出在代码运行之前就确定
      1. 模块的复制
      • CommonJs:导入的模块是被复制的,即每次 require() 都会生成一个新的实例,模块内部的状态不会被共享
      • ES6 模块:模块是单例的,即每个模块只会被加载一次,后续的导入都会返回同一个实例,模块内部的状态是共享的
      1. 动态导入
      • CommonJS:不支持动态导入,require() 的参数必须是静态字符串
      • ES6 模块:支持动态导入,可以在运行时根据条件动态地导入模块,使用 import() 函数实现
      1. 顶层作用域
      • CommonJs:模块中的代码在一个单独的作用域中运行,模块内部的变量不会影响到全局作用域
      • ES6 模块:模块中的代码默认在严格模式下执行,模块内部的变量和函数默认不会被绑定在全局作用域,需要显示地使用 exportwindow 对象绑定到全局作用域
      1. 浏览器兼容性
      • CommonJS:主要用于服务器端的 Node.js 环境,浏览器端需要使用工具(如 Browserify、Webpack)进行转换才能使用。
      • ES6 模块:现代浏览器原生支持 ES6 模块,无需额外的转换工具
  9. 生成器 Generator

    Generator 函数会返回一个遍历器对象,即具有 Symbol.iterator 属性,通过 yield 关键字可以暂停函数返回的遍历器对象的状态(所以可以用 for...of 遍历)

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
    }
    var hw = helloWorldGenerator()
    
    console.log(hw.next())
    // { value: 'hello', done: false }
    console.log(hw.next())
    // { value: 'world', done: false }
    console.log(hw.next())
    // { value: undefined, done: true }
    

使用场景:

  • 异步编程:Generator 函数可以与异步操作结合使用,通过使用 yield 和 next 控制生成器的执行流程。它为异步编程提供了一种更简洁、易读和易理解的方式。在 ES6 之前,Generator 函数常被用于实现基于回调的异步编程模式,而在 ES6 之后,它通常与 async/await 结合使用
  • 控制流管理:Generator 函数允许我们在迭代的过程中显式地控制执行流程。通过 yield 语句,我们可以在每个步骤之间进行流程控制,使得代码更加可读和可维护
  • 数据流处理:Generator 函数可以作为数据流的生成器和消费者。我们可以使用 yield 从生成器函数中产生数据,并使用 next 将数据传递给生成器函数。这种数据流处理的方式非常灵活,可以用于实现各种数据处理和转换的场景
  1. 装饰器 Decorator 装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论(可用于装饰器模式,可以类比手机壳🐶)
  • 类的装饰: 比如让A类拥有B类的属性,在A类头上加上 @B
  • 类属性的装饰:@readonly
  • 使用案例:MobX
  1. Set、Map、weakSet、WeakMap

  2. Proxy

    1. 使用案例:vue3 的双向数据绑定实现原理(代替了 Object.defineProperty
    2. Object.definePropertyProxy

    Object.defineProperty 可以在对象上修改或新增属性,并设置属性的描述符:

    // 对象内置了get和set,可以重写
    let obj = {
      name: 1,
      get age() {
        return 22
      },
      set age(value) {
        obj.name = value
      }
    }
    obj.age = 300
    console.log(obj.age) // 22
    console.log(obj.name) // 300
    
    // 显示重写
    Object.defineProperty(obj, 'a', {
      value: 'test', // 给obj的a属性赋值为'test'
      writable: true, // 属性是否可写,如果不设置这个属性,obj.a将不能修改值
      enumerable: true, // 属性是否可枚举
      configurable: false, // 属性是否可删除或修改
      get() { 
        // ...
      },
      set(val) { 
        // ...
      },
    })
    

    Proxy 用于创建对象的代理,可以拦载并自定义对象的操作:

     const proxy = new Proxy(target, handler)
     // target:要拦截的目标对象
     // handler:拦截属性的操作方法:get、set、has、deleteProperty、ownKeys...
    
     // 示例
     let validator = {
      set: function (obj, prop, value) {
        if (prop === 'age') {
          if (!Number.isInteger(value)) {
            throw new TypeError('The age is not an integer');
            if (value > 200) {
              throw new RangeError('The age seems invalid');
              // 对于满足条件的 age 属性以及其他属性,直接保存obj[prop]= value;
            };
          }
        }
      }
    }
    let person = new Proxy({}, validator);
    person.age = 100;
    person.age // 100
    person.age = 'young'//报错person.age=300 //报错
    

    区别: 🔥

    • 代理的粒度不同:Object.defineProperty 只能代理属性,Proxy 代理的是对象 & 属性(可以代理对象的所有属性,不需要遍历一个个设置 gettersetter

    • 是否破坏原对象: Object.defineProperty 的代理行为是在破坏原对象的基础上实现的 而 Proxy 不会破坏原对象,只是在原对象上覆盖一层代理

    • 代理数组属性:Object.defineProperty不适合监听数组,而 Proxy 可以代理数组属性

    • 代理范围:Object.defineProperty 只能代理属性的 get 和 set,Proxy 可以代理更多的 行为(如 delete 操作、构造函数等)

    • 兼容性:Object.defineProperty 容性较好,而 Proxy 是 E56 新增的特性(这也是为什么 vue3 不支持IE的原因之一)

      扩展: vue2 和 vue3都是怎么监听数组的?【todo, orz】

    1. Reflect

      若需要在 Proxy 内部调用对象的默认行为,建议使用 Reflect ,它是 ES6 中操作对象提供的新 API。特点:

      • 只要 Proxy 对象具有的代理方法,Reflect 对象全部具有,以静态方法的形式存在
      • 修改某些 Object 方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回 false )
      • 让 Object 操作都变成函数行为
    2. Proxy 的使用场景

      • 拦截和监视外部对对象的访问
      • 降低函数或类的复杂度
      • 在复杂操作前对操作进行校验或对所需资源进行管理
  3. es6+ 最新特性:findLast、findLastIndex、toSort、weakRef...【todo, orz】🔥

其他面试常考点

答案整理 【todo, orz】

  1. ===== 的区别,类型转换
  2. this 的指向:new、显示(call、apply、bind的区别、实现)、默认、隐藏(箭头函数、立即执行函数需要注意些什么)
  3. typeofinstanceofObject.prototype.toString.call() 的区别
function getType(data) {
      // 如果是基本类型,直接用typeof
      const type = typeof data
      if(type !== 'object') {
          return type
      }

      // (\S+)用中括号括起来的是捕获组,这里也就是[object 类型]里的类型($1,第一个捕获组)
      // \S 表示任意非空白字符,>=1个
      return Object.prototype.toString.call(data).replace(/^\[object (\S+)\]$/, '$1')
 }

  // 测试
  getType(1)
  getType({})
  getType(null)
  getType(new Date())
  getType([1,22])
  1. instanceof 实现原理
function myInstanceof(left, right) {
  // 如果是基础类型的话,instanceof判断不出来
  /*
  'sds' instanceof string // Uncaught ReferenceError: string is not defined
  'sds' instanceof String // false
  */
 if(typeof left !== 'object' || left === null) {
   return false
 }

 // 顺着原型链去找,找得到就是true
  //    let proto = Object.getPrototypeOf(left)
     while(true) {
      // 找到尽头都没找到
      if(left === null) return false
      if(left.__proto__ === right.prototype) return true

      left = left.__proto__
     }
  }

  // 测试
  let Car = function() {}
  let car1 = new Car();
  console.log(myInstanceof(car1, Car))
  console.log(myInstanceof(car1, Object))


  let num = new Number(1)
  console.log(myInstanceof(num, String))
  console.log(myInstanceof(1, Number))
  1. JS 继承方式

  2. JS面向对象三大特性,举例(封装、继承、多态)

  3. 变量提升和函数提升(函数表达式不会提升)

  4. DOM & BOM

  5. 事件冒泡、捕获、委托

  6. JS 设计模式

  7. 函数式编程、数柯里化、纯函数、高阶函数

  8. symbol 的作用、使用场景

  9. JS 的深浅拷贝,实现一个 deepClone

  10. toString & valueof

  11. JS 的错误捕获方法

  12. 垃圾回收的方法(引用计数、标记清除)

  13. 内存泄漏的场景、治理

  14. babel 的原理

  15. TS:JS 的超集,支持面向对象编程的概念(类、接口、继承、泛型)

    1. 特性(类型批注/推断、擦除、接口、枚举、Mixin、泛型、元组、名字空间...)
    2. 高级类型(交叉、联合、类型别名/索引、约束、映射类型、条件类型...)
      1. 类型别名 type 和接口的区别:接口只能定义接口类型,但是 type 声明可以定义对象、交叉/联合、原始类型等
    3. TS 的 class :public、private、protect、readonly、static...
  16. 常见的字符串和数组API真的建议好好掌握,算法和编程题用得到

    1. 数组:
      • push、pop、shift、unshift、find、findIndex、reverse、sort、join
      • slice:[a, b),b可省略
      • splice:(start, deleteCount, item1, item2...)
        • start: 修改的起始索引
        • deleteCount:可省略,如果此时后面有参数,就是插入(插入位置也是从 start 开始)
      • 迭代:forEach、map、some、every、filter(注意区别)
    2. 字符串:
      • slice:[a, b),b可省略
      • substring:[a, b),b可省略,a比b大的话会自动交换
      • substr: [a, length)
      • 参数是负数的话,slice 相当于从字符串末尾开始处理,substring 会当成0处理
      • 匹配:match、search、replace
      • split
  17. “子子孙孙,无穷尽也”......