# JavaScript

# 概念

  • 基础类型赋值只改变本身,引用类型赋值全部修改(栈内存中保存的地址指针指向的是堆内存中同一个位置)
  • 所有微任务总会在下一个宏任务之前全部执行完毕,直至微任务队列清空,过程中如果有微任务产生微任务会继续执行新的微任务
  • alert弹出的数据都会转化成字符串
  • js是单线程,浏览器是多进程
  • JSON.stringify会过滤掉undefined,不会过滤掉null

# 1.说一下闭包/闭包解决了什么问题?

闭包就是能够读取其他函数内部变量的函数(函数执行会形成私有上下文,这种保存私有变量的机制就是闭包) 用来解决函数外部因作用域访问不到函数内部的问题。 (即子函数在外部调用,子函数所在的父函数的作用域不会释放) (函数作用域是独立的,封闭的。但是闭包是能够读取其他函数内部变量的一个函数) 我们在函数中嵌套一个函数,里面的函数去访问外面函数的变量,被访问的变量会始终保存在内存中

function init() {
    var name = "Mozilla";
    // name 是一个被 init 创建的局部变量
    function displayName() {
    // displayName() 是内部函数,一个闭包
        alert(name); //使用了父函数中声明的变量
    }
    displayName();
}
init();
1
2
3
4
5
6
7
8
9
10

# 不恰当使用闭包会造成什么问题

会造成内存泄漏(要把变量定义在函数内,而不是全局变量,这样程序执行完就会销毁变量)。 防抖和节流都有用到闭包

# 常见问题

// var变量提升相当于提前声明了i
// var i = 0;
for (var i = 0; i < 6; i++) {
    setTimeout(() => {
    console.log(i) // 1s后输出6个6=>不能用const,用let是1s后输出0-5
    }, 1000)
}
1
2
3
4
5
6
7

# 2.如何解决回调地狱?(3种方式)

promise,async/await,generator

# 3.说一下事件委托?

利用事件的冒泡机制来实现事件委托 不在元素上直接设置监听函数,而是在父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上的事件被触发。举例:ul,li监听。 ( 子元素事件冒泡到父元素,需要父元素也有该事件)

# 事件捕获和冒泡过程

  • 事件捕获(老版本浏览器不支持,用得较少):从window一级一级往下,最后到元素接收到=》从外到内
  • 事件冒泡(IE):从元素一级一级往上,到window接收=》从内到外

# 冒泡和捕获哪个先?

先捕获后冒泡

# dom事件流?

  • window->document->body->div->body->document->window
  • 事件流的典型应用就是事件委托。事件委托利用了事件冒泡,把事件加到父级上,就可以管理一类型的所有事件。 (ul和li,把事件加到ul上,可以监控到li)

# event.preventDefault和event.stopPropagation有什么区别

  • event.preventDefault()可以阻止默认事件但是允许冒泡事件
  • event.stopPropagation()可以阻止冒泡但是允许默认事件

# *浏览器的事件模型?

  • 事件模型的过程主要分三个阶段:捕获阶段,目标阶段,冒泡阶段
  • addEventListener(第三个参数如果是true,就代表在捕获阶段执行,否则在冒泡阶段执行=>对应useCapture:true) =>第三个参数也可以是个options

# 4.说说深浅拷贝?

(深浅拷贝区别是对于引用类型如object,array而言的)

  • 深浅拷贝,基本类型的时候,改变都不会使原数据改变。
  • 引用类型,浅拷贝改变会使原数据改变,深拷贝不会。 (浅拷贝和赋值也有区别,赋值和原数据指向的是同一对象,浅拷贝不是)
  • 因为引用类型,浅拷贝拷贝的是内存地址,深拷贝拷贝的是值,但是内存地址不同,所以不会。
  • 浅拷贝实现是通过对对象各个属性依次进行拷贝,拷贝的是引用

# 浅拷贝方式

  1. Object.assign() =》当对象只有一层是深拷贝
  2. es6的扩展运算法...
  3. slice() // 通过对象序列化和字符串反序列化也可实现

# 深拷贝实现是通过递归的方式将值进行拷贝

深拷贝方式:

  1. JSON.stringify() 函数,日期,正则在JSON.stringify时都会出现问题
  2. 遍历递归来拷贝(非原型属性)
var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}]
var new_arr = JSON.parse( JSON.stringify(arr) );
1
2

实现浅拷贝

function clone(target) {
    if (target === null) {
        return null;
    }
    // 判断原来是数组还是对象
    let cloneTarget = Array.isArray(target) ? [] : {}
    for (const key in target) {
        if (target.hasOwnProperty(key)) { // 判断是否存在该属性,不包括原型链
            cloneTarget[key] = target[key]
        }
    }
    return cloneTarget;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

实现深拷贝

 function deepCopy(obj) {
          var result = Array.isArray(obj) ? [] : {};
          for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
              if (typeof obj[key] === 'object') {
                result[key] = deepCopy(obj[key]);   //递归复制
              } else {
                result[key] = obj[key];
              }
            }
          }
          return result;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
// 利用 WeakMap 解决循环引用
let map = new WeakMap()
function deepClone(obj) {
  if (obj instanceof Object) {
    if (map.has(obj)) {
      return map.get(obj)
    }
    let newObj
    if (obj instanceof Array) {
      newObj = []
    } else if (obj instanceof Function) {
      newObj = function() {
        return obj.apply(this, arguments)
      }
    } else if (obj instanceof RegExp) {
      // 拼接正则
      newobj = new RegExp(obj.source, obj.flags)
    } else if (obj instanceof Date) {
      newobj = new Date(obj)
    } else {
      newObj = {}
    }
    // 克隆一份对象出来
    let desc = Object.getOwnPropertyDescriptors(obj)
    let clone = Object.create(Object.getPrototypeOf(obj), desc)
    map.set(obj, clone)
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        newObj[key] = deepClone(obj[key])
      }
    }
    return newObj
  }
  return obj
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 5.freeze函数?

Object.freeze表示冻结该对象,被冻结的对象无法被修改(包括新增属性,删除属性,以及可枚举性,可配置性等),但是允许访问。var a = Object.freeze({})

# 6.执行上下文

  • js用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表。
  • 当函数调用时,会入栈一个新的执行上下文,函数调用结束,这个执行上下文会出栈

# *7.js有哪些代码规范

  • 尽量用===代替==,防止转换的时候出现预期外的异常
  • switch 必须带有default
  • if 使用大括号,不要省略
  • 分清楚常量和变量=>const,let
  • 使用分号(避免代码格式化的时候出现预期外的问题)

# 8.栈和堆的区别?

let a = 20; 都存在栈中 let a = {m: 29} a存在栈中,{m:29}作为对象存在堆中,当我们要访问堆内存中的引用数据类型,首先要从栈中获取该对象的地址引用。

  • 栈存储基础数据类型,堆存储引用类型
  • 栈按值访问,堆按引用地址访问
  • 栈存储的值大小固定,会自动分配空间,堆存储的值大小不固定,可动态调整

# 堆内存?

  • 不能直接访问和操作堆内存,只能操作对象在栈内存的引用地址,通过这个地址可以找到堆内存中的对象
  • 比如引用类型的赋值操作,是把原来对象的栈内存地址拷贝一份给新的对象,他们都指向同一个堆内存对象,所以成功给这个对象赋值

# 9.让数组乱序/数组打乱?

_.shuffle([1, 2, 3, 4]);//需要通过第三方库lodash function shuffle(arr) { return arr.sort(() => { return (Math.random() - 0.5); }); }

# 10.什么是数组扁平化?

数组扁平化就是将一个多维数组转换为一个一维数组。

  1. es6的flat arr.flat(Infinity) 数组才有的属性
  2. 数组转化成字符串 使用toString再用split变成以逗号分割的数组(再用map遍历转成数字返回) arr.toString().split(',')
  3. 循环递归=>循环判断子元素是否存在数组,如果是就把所有元素展开放到一个空数组中再重新赋值,直到没有数组 while(arr.some(item => Array.isArray(item)) { arr = [].concat(...arr) }

# flatMap

会返回扁平化深度为1的数组,与flat区别就是参数传的是一个函数

const arr1 = [1, 2, [3], [4, 5], 6, [7,8,9]];
console.log(arr1.flatMap(num => num)); // [1,2,3,4,5,6,7,8,9]
1
2

# 11.js基本数据类型?

  • string,boolean,number,undefined,null,symbol(es6新增),(bigInt=>大于2^53 - 1的数,就是最大安全整数Number.MAX_SAFE_INTEGER)
  • 引用数据类型:对象(Array,Math,Map,Set,RegExp),函数
  • 转数字规则:null为0,undefined为NaN,symbol报错,字符串非数字(或进制)转NaN。NaN不等于自身

# typeof可以判断null吗

不能,null通过xx === null判断=》typeof null为null是早期的bug (typeof判断函数为function,其他都是Object)

# 判断一个数是整数?

  • %1 判断是不是0
  • Number.isInteger()

# 2^53次方怎么表示?

  • 2 ** 53
  • Math.pow(2,53)

# bigInt怎么表示?

整数后面加n即可

# isNaN和Number.isNaN区别?

(NaN通过isNaN(123)来判断(NaN指的是not a number,number的范围是-2^53~2^53)) isNaN会把参数转成数值,不能转换的都会返回true Number.isNaN会先判断是否是数字,是数字再判断是否是NaN

# Null和Undefined的区别?

  • null表示定义了一个空值, 变量声明了没有赋值就是undefined
  • null参与计算等于0,undefined参与计算返回NaN(Number)

# 12.数组常用方法?

push(),pop(),shift(),unshift(),splice(),sort(),map() 会改变数组的方法:push(),pop(),shift(),unshift(),splice(),reverse(),sort()

# 13.*js语言特性?

  • 可以在浏览器运行
  • 不用预编译,直接解析执行代码

# 14.改变this指向(apply,bind,call)?

call,bind,apply都可以,箭头函数和proxy也可以改变=》proxy指向代理对象

fn.call(obj, 1,2) call接收一个参数列表
fn.bind(obj,1,2)也是接收一个参数列表,但是返回的是一个函数
fn.apply(obj,[1,2])apply接收的是一个参数数组
1
2
3

# 总结

  • call和bind都是接收一个参数列表,apply接收的是一个参数数组
  • bind返回的是一个函数,需要执行才生效

# 15.const定义的数据可以改变吗?

  • 定义的是基本类型不可以改变,定义的是引用类型就可以改变,比如对象
  • const定义引用类型(对象,数组),不能修改指针指向的地址,但可以修改地址指向的数据

# 16.异步加载的几种方式?

  • script的defer属性:defer属性规定是否对脚本执行进行延迟,直到页面加载为止。可以保证脚本按顺序执行(如果脚本不改变文档的内容,可以通过这种方式加快文档处理的速度,不是所有浏览器都支持)
  • script的async属性:async是h5新增特性,只适用外部引入的脚本,一旦脚本可用会异步执行。不能保证脚本按顺序执行,不是所有浏览器都支持
  • $(document).ready()。必须引入jquery

# defer和async区别?

js脚本分成加载、解析、执行几个步骤,脚本加载且执行会阻塞dom渲染(所以js一般放最后)

# 相同点

都是异步加载

# 不同点

  • async加载完立即执行,可能会阻塞dom解析
  • defer加载后延迟到dom解析完才执行(在domContentLoaded之前)

# 17.事件循环?| js的执行机制?

事件循环分成同步和异步=》微任务和宏任务都是异步的 (js是单线程,一次只能执行一个任务,执行机制就是事件循环。浏览器是多线程) (先执行一个宏任务=》)执行代码,过程中把宏任务和微任务分别放入各自的任务队列,等代码块执行完就立即执行微任务队列中所有的微任务, 当所有微任务都执行完就开始下一次宏任务

# 执行微任务过程中产生了微任务

同样会加到当前的微任务队列中,等微任务队列都清空了才开始下次宏任务

# 为什么区分微任务和宏任务?

  • 微任务是线程之间的切换,不用进行上下文切换,一次事件循环可以完成所有微任务。

  • 宏任务是进程之间的切换,每次需要切换上下文,一次事件循环只能执行一个宏任务。

  • 目的为了实现插队。

  • 宏任务:js脚本,定时器,IO操作,ajax=》宏任务不一定都是异步任务,如js脚本

  • 微任务:Promise.then,catch,finally

# 为什么微任务会比宏任务先执行?

因为当主线程代码执行完后,在事件循环执行之前,会尝试dom渲染,微任务是在dom渲染之前执行,宏任务是在dom渲染完成后执行。

# *18.如何原生实现instanceof?

function fakeInstanceOf (instance, parent) {
    if (typeof instance !== "object" && typeof instance !== "function") return false;
    while (instance.__proto__) {
        if (instance.__proto__ === parent.prototype) {
            return true;
        }
        instance.__proto__ = instance.__proto__.__proto__;
    }
    return false;
}
1
2
3
4
5
6
7
8
9
10

# 19.什么是函数柯里化(bind方法就是)?

函数柯里化实际是一种转换,返回的是一个函数。是将多变量函数拆解为单变量的多个函数。f(a,b,c)=>f(a)(b)(c) a,b,c各自都是一个函数内的变量

# 20.typeof判断数据类型的原理

typeof是通过判断存储的机器码的第三位来进行类型判断(全0表示null,全0也表示对象,所以不能用typeof判断null)

# 21.this关键字在不同环境下的指向?

  • this在浏览器指向window

  • 普通函数中的this指向window

  • 定时器的this指向window

  • 箭头函数的this取决于外部环境

  • 事件中的this指向事件的调用者

  • 构造函数和原型对象中的this都是指向new出来的实例对象

  • 类中的this指向构造器new出来的实例对象

  • 谁触发函数,函数里的this就指向谁

fun() // window.fun()里面的this指向window
1
  • 多层对象调用,函数被外层对象调用,指向的也是上一级对象
var obj = {
    method: {
        fn: function() {
            console.log(this)
        }
    }
}
obj.method.fn() // 指向method
1
2
3
4
5
6
7
8
  • 箭头函数调用,箭头函数本身没有this,实际调用的是上一级的this
var obj = {
    fun: ()=> {
        console.log(this)
    },
    fun2: function() {
        console.log(this)
    }
}
obj.fun() // window
obj.fun2() // obj
1
2
3
4
5
6
7
8
9
10

# 22.原型和原型链(构造函数<==>原型 实例=>原型)

构造函数通过prototype指向原型,原型通过constructor指向构造函数,原型(Parent.prototype)通过_proto_指向原型的原型(Object.prototype)

# 原型链

当访问一个实例对象的属性时,如果本身没有这个属性,就会去原型对象中查找,一直找到Object为止,原型间一层层的关系就构成了原型链。最终指向null (不是所有对象都有原型(通过对象创建的对象都有原型,空对象及Object.prototype(原型的最顶端)没有原型)) 实例(p)=>原型(Parent.prototype)=>原型(Object.prototype)=>null // 通过_proto_一层层往上找

# Object.create和原型链(MDN)

  • object.create(proto)是创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)
  • 每个对象拥有一个原型对象。对象以原型为模版,继承了方法和属性。原型对象可能也有原型,一层一层以此类推,就构成了原型链
  • 这些属性和方法定义在构造函数的prototype中,而不是对象实例本身
  • js通过在对象实例和构造器之间建立一个链接_proto_在构造器中找到这些属性和方法
  • 每个实例对象都从原型中继承了一个constructor属性,指向了构造函数
  • prototype是内部属性,外部无法直接访问。但是继承者的p._proto_和被继承的Parent.prototype指向统一对象
  • 顶层原型对象是object,object的原型对象是null(实例的_proto_等于原型的prototype)
  • js中的继承通过原型链继承实现,所以js是基于原型的语言。java是面向对象语言,创建对象后会把类的属性和方法复制到对象中
  • 函数默认有prototype属性,prototype中有constructor属性,指向函数本身

# 构造函数与普通函数最核心区别

构造函数通过new来调用,普通函数不需要new 7NK5E8.png (opens new window)

# 23.js是单线程,单线程有什么好处?

不用在意线程状态同步问题,没有死锁的情况。

# 24.querySelector返回值是什么?querySelectorAll呢?

querySelector返回的是匹配的第一个元素,querySelectorAll返回的是一个nodeList

    for (var div of divs) { 遍历nodeList
        div.style.color = "blue";
    }
1
2
3

# 25.函数传参是引用吗?修改形参会影响实参?

  • 函数传参是引用。 不影响。会开辟一个新的内存地址指向这个参数,修改的是这个新地址指向的值。
  • 基本类型存储在栈中,引用类型存储在堆中,引用类型存储的是一个指针,指向存储对象的内存地址。
  • 基本类型的值可以直接访问,引用类型的值通过指针去访问(js不允许直接访问堆内存中的位置)。

# 26.如何判断变量是否是数组

arr instanceof Array
Object.prototype.toString.call(arr) === '[object Array]' //同样也可以用来判断对象类型 
Array.isArray(arr)
1
2
3

# 27.如何把对象转成key/value的二维数组

object.entries({a:3})

# 28.说几种遍历数组的方法

forEach(缺点中途无法退出),map,some,every,filter,for of,

# 29.js的执行过程

  • 词法分析=》输入的字符串会变成token(var a = 2 会被分割成4个单元 var|a|=|2)
  • 语法分析=》token解析成抽象语法树
  • 解释执行-=》(后续遍历AST)会在执行上下文执行

# 30.一个超长字符串能存在栈内存中吗?

不能,因为字符串数据一直都是存在堆中的,栈只存了地址指针 数字=》小整数存在栈中,其他存在堆中 只有固定大小的对象和值才能在栈上分配空间,string内部是可变的字符数组

# 31.for of | for in实现原理

都是基于js的迭代器实现 for in是利用了js中的in操作符,当使用in在某个对象上,如果该对象中存在此属性的原型链就会返回true,否则返回false for of是基于js的生成器(generate)实现

# 32.new一个对象发生了什么?

  • 新建一个空对象
  • 为新对象添加属性_proto_并指向构造函数原型
  • 绑定this,执行构造函数
  • 如果构造函数没有返回对象,就返回这个新对象

# 手写new

function create() {
// 创建⼀个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执⾏构造函数
let result = Con.apply(obj, arguments)
// 如果构造器没有手动返回对象,则返回第一步的对象
return typeof result === 'object' ? result : obj
}
1
2
3
4
5
6
7
8
9
10
11
12

# 33.==和===有什么区别?

前者只要求值相等,后者要求值和类型都要相等

# 34.实现一个同步的sleep函数

function sleep(ttl) {
  const now = Date.now();
  ttl *= 1000;
  while (Date.now() - now < ttl) {}
}
1
2
3
4
5

# 35.字符串去重

const str = 'aaavvvdwww'
var rs = [].filter.call(str, (item, index, origin) => origin.indexOf(item) == index).join('');
item当前字符、index当前字符索引、origin原字符串
1
2
3
let newArr = [...new Set(str.split(""))].join(""); 最优解
let newArr = Array.from(new Set(str.split(""))).join("")
1
2

# 36.怎么判断两个对象相等(转成字符串判断)

JSON.stringify(obj)==JSON.stringify(obj2)

# 37.如何生成100个元素为1的数组?

  • Array(100).fill(1); => 相类似的是Array(100).map(x=>1)将会创建一个稀疏数组 [empty * 100]
  • Array.from(Array(100), x => 1); | Array.from({length: 100}, x=>1)
  • Array.apply(null, Array(100)).map(x => 1);

# 38.typeof和instanceof的区别

  • typeof判断基础数据类型,null除外
  • instanceof判断引用数据类型

# instanceof原理=》基于原型链

内部通过原型链的方式来判断是否为构造函数的实例

# 手写instanceof

let myInstanceof = (target,origin) => {
    while(target) {
        if(target.__proto__===origin.prototype) {
            return true
        }
        target = target.__proto__
    }
    return false
}
let a = [1,2,3]
console.log(myInstanceof(a,Array));  // true
console.log(myInstanceof(a,Number));  // false
1
2
3
4
5
6
7
8
9
10
11
12

# typeof和instanceof常见判断

  • typeof判断引用类型除函数都是object
  • instanceof不能判断基本类型 111 instanceof Number =》 false

# 39.字符串翻转

let a = 'test'
console.log(a.split("").reverse().join(""))
1
2

# 手动实现reverse函数

function reverse(s) {
 let r = "";
 for (const c of s) {
  r = c + r;
 }
 return r;
}
1
2
3
4
5
6
7

# 40.手写promise.race

Promise.race = (promiseArray) => {
    return new Promise((resolve, reject) => {
        promiseArray.forEach((item) => {
            Promise.resolve(item).then(
                (val) => {
                    resolve(val);
                },
                (reason) => {
                    reject(reason);
                }
            );
        });
    });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 41.js数组哪些方法改变自身,哪些方法不改变

  • 不改变原数组的方法:concat/join/map/forEach/filter/slice/findIndex
  • 改变原数组的方法:push/unshift/pop/shift/sort/splice/reverse =>就是vue中重写的数组

# 42.说说Object.defineProperty

// 普通
let o = {};o.a=3;
// Object.defineProperty
let o = {};
Object.defineProperty(o, 'a', {
 configurable: false, // 是否可删除,默认false删除无效
 enumerable: false, // 是否可枚举 默认false
 writable: true, // 是否可更改,默认false修改无效
 value: 3 默认undefined
})
// 可拦截对象属性进行get,set操作
Object.defineProperty(o, "a", {
 configurable: false,
 enumerable: false,
 get() {
  return this._a;
 },
 set(a) {
  this._a = a * 10;
 },
});
o.a=1; // 赋值调用set方法,取值调用get方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 43.如何全部替代一个子串为另一个

  1. str.split('foo').join('bar')
  2. str.replaceAll('foo', 'bar'),在ESNext中,支持性不好

# 44.0.1+0.2不等于0.3?

计算机对于浮点数无法准确表达为二进制,此时再转成10进制就不是我们预期的结果0.2+0.4,0.7+0.1

# 45.类数组转数组

类数组指类似数组的对象。有length属性 (key是0,1,2这样的,类似数组的下标。常见的有arguments) Array.from(arr)可以将类数组转数组

# 46.//TODO

# 47.js数组去重(原生js)

Array.from(new Set(arr)) arr.filter((item, index) => { // 返回当前元素在数组中第一个索引,重复出现的返回都是false,就被过滤掉了 return arr.indexOf(item, 0) === index; })

# 48.什么是作用域?什么是作用域链?

作用域就是变量的可用范围。分成全局作用域,块级作用域({}只在花括号内) 当访问一个变量,首先会在当前作用域查找,找不到就会去上一级作用域查找,直到全局作用域,这个过程就是作用域链

# js有哪些块级作用域

let,const,以及try-catch的catch语句

# 49.定时器准时吗

定时器实际可能不准时,受事件循环影响

# 为什么setTimeout有最小时延4ms

(不同浏览器的最小时延不一致,chrome最小时延是1ms)定时器嵌套的层级很多超过了5层那么最低是4ms 设置延迟过低会导致 CPU-spinning(会导致计算机没办法进入低功耗模式,这样耗电就比较严重),经过测试后选定了最小时延4ms

# 如何解决最小时延

可以通过window.postMessage()=>也是宏任务,和addEventListener('message')结合模拟定时器功能

# 50.promise.all错误处理

场景:获取多个用户的信息,即使一个用户失败,也不影响其他用户信息的获取=》此时使用Promise.allSettled; 在所有promise都fulfilled或rejected后,返回一个数组

[
{ status: "fulfilled", value: result} // 成功响应
{ status: "rejected", reason: error} // 表示错误
]
1
2
3
4

原生错误处理:对每个promise进行错误处理,处理后返回res或err,最终到promise.all的还是一个数组,因为错误都被捕获了,所以不会一个reject就挂掉

async function handleAllFunc(fun){
    try{
        let res = await fun
        return res
    }catch(err){
        return err
    }
}
let requestArr = [p1(), p2(), p3()]
Promise.all(requestArr.map(item => handleAllFunc(item))).then(res => {
    console.log('结果', res);
}).catch(err => {
    console.log('错误', err);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 51.异步编程的实现方式

  1. 回调函数 简单,但不利于维护,耦合度高(回调地狱)
  2. promise 链式调用
  3. async 结构清晰更容易理解

# 52.js如何实现多线程

通过worker类 使用这个类的时候,会向浏览器申请一个新的线程来单独执行一个js

# 53.js 模块化机制

没有模块化:

  • 变量和方法不容易维护,容易污染全局作用域
  • 加载资源通过script从上到下 有模块化就解决了以上问题

# amd和cmd(sea.js推广过程产生)区别

  • amd加载完模块立即执行,cmd加载完等require才执行
  • amd是异步引入模块,cmd是同步引入模块

# umd

  • umd兼容cjs和esm=》以amd为基础,加了特殊处理去兼容cjs (umd和esm前后端通用)

# cjs(commonjs)和esm(es module)区别?

  • cjs是module.export导出,require导入,esm是export导出,import
  • cjs是动态导入,esm是静态导入支持tree shaking
  • cjs是被加载时运行,esm是编译时候运行。
  • cjs输出的是值的浅拷贝,esm输出值的引用(输出值拷贝,输出后改变值,输出值不变。输出值引用会变)
  • cjs会缓存,第一次被加载会完整运行整个文件并输出一个对象,拷贝在内存中,下次加载文件,直接去内存中取

# esm,cjs,amd,cmd,umd定义

esm和cjs都是用来模块规范=》导入导出 amd(异步模块定义),cmd和umd(都是通用模块定义)

# esm特性

自动采用严格模式,忽略use strict esm是通过cors去请求外部js模块 esm的script标签会延迟执行脚本

# 54.剪贴板相关操作

可通过Clipboard API来操作(是异步的)=》navigator.clipboard.writeText(text);

# 不支持clipboard API的浏览器

先通过Selection API选中,再通过document.execCommand('copy')进行拷贝

# 59.js中的数组特点|数组如何存储

  • 有两种存储方式,快数组和慢数组。
  • 初始化空数组会用快数组(默认空数组初始化大小为4),快数组使用连续的内存空间,当数组长度达到最大时,会进行动态扩容(老容量*1.5+16)。
  • 转成慢数组,以哈希表(key,value)的方式存储数据(不需要连续的内存空间)

# 快数组什么时候转成慢数组

新加入的索引值比当前容量大等于1024或者快数组新容量是扩容后容量的三倍还多的时候都会由 快数组 转成 慢数组

# 60.input事件和change事件区别

  • input:只要在input框内输入内容就触发
  • change:只有input框内内容改变且失去焦点才触发

# 61.with作用

可以用来对对象的属性进行解析,但是不推荐使用,存在数据泄漏的问题 =>如果在with里面赋值一个不存在该对象上的属性,会自动创建全局属性,而不是给这个对象添加新属性

a.age = 18
a.name = "aa"
// 等价于
with(a) {
    age = 18
    name = "aa"
}
1
2
3
4
5
6
7

# 62.e.target和e.currentTarget区别

  • 绑定的事件所在元素没有子元素,两者一样
  • 事件绑定在父元素 e.currentTarget无论点击父元素还是子元素都正常执行(e.currentTarget都是parent) e.target点击父元素无错,点击子元素报错(因为事件绑定在父元素上,此时e.target是children)

# 63.json相关

以下代码输出什么?
const obj = {
  a: 3,
  b: 4,
  c: null,
  d: undefined,
  get e() {},
};
console.log(JSON.stringify(obj));
// {"a":3,"b":4,"c":null} 对于undefined和function会被忽略掉
1
2
3
4
5
6
7
8
9
10

# 64.剩余参数和arguments对象区别是什么

剩余参数只包含没有对应形参的实参,而arguments对象包含了传给函数的所有实参 arguments对象不是一个真实数组,而剩余参数是真实的数组,所以剩余参数可以直接使用数组的所有方法,arguments需要转成真实数组才能使用 =>arguments是类数组

# 65.数组转树

const arr = [{id: 1, title: 1, parent: 0}, {id: 2, title: 2, parent: 1}, {id: 3, title: 3, parent: 1}]
function transTree(arr, id) {
    const res = []
    for (const item of arr) {
        if (item.parent === id) {
            res.push({...item, children: transTree(arr, item.id)})
        }
    }
    return res;
}
console.log(JSON.stringify(transTree(arr, 0)))
1
2
3
4
5
6
7
8
9
10
11