# 一.JS 基础

# 1.数据类型

# 1.1 种类

原始类型有那几种?null 是对象吗?

  • 6 种:undefined、null、string、boolean、number、symbol
  • 原始类型存储的都是值,是没有函数可以调用的,能调用是因为被强制转换成了 string 类型,也就是对象类型 '1'.toString(),number 是浮点类型的

# 1.2 判断

  • 1.typeof

    • 只能判断基本数据类型:Number String undefined ,null,symbol Boolean;
    • null 返回 object
    • 对于引用数据类型除了 function 都返回 object
  • 2.instanceof

    • 用来判断 A 是否是 B 的实列,返回值为 true 或 false,instanceof 检查的是原型
    • 对于原始类型来说,你想直接通过instanceof来判断类型是不行的,当然我们还是有办法让instanceof判断原始类型的
    class PrimitiveString {
      static [Symbol.hasInstance](x) {
        return typeof x === "string"
      }
    }
    console.log("hello world" instanceof PrimitiveString) // true
    
    1
    2
    3
    4
    5
    6

    你可能不知道Symbol.hasInstance是什么东西,其实就是一个能让我们自定义instanceof行为的东西,以上代码等同于typeof 'hello world' === 'string',所以结果自然是true了。这其实也侧面反映了一个问题,instanceof也不是百分百可信的。

  • 3.toString

    • 是 Obejct 的原型方法,对 Object 对象,直接调用 toString()就能返回[Object Object].而其他对象,则需要通过 call/apply 来调用才能返回正确的类型信息
  • 4.hasOwnProperty

    • 方法返回一个布尔值,指示对象自身属性中是否具有指定的属性,该方法会忽略掉那些从原型上继承到的属性。
  • 5.isProperty

    • 方法测试一个对象是否存在另一个对象的原型链上。
  • 6.constructor

    • 返回创建该对象的函数,也就是我们常说的构造函数

# 1.3 转换

首先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串
原始值 转换目标 结果
number 布尔值 除了 0、-0、NaN 都为 true
string 布尔值 除了空字符串都为 true
undefined、null 布尔值 false
引用类型、Symbol 布尔值 true

基本数据类型除了 Symbol,0、-0、NaN,‘’,undefined、null 其他基本数据类型值以及引用类型都为 true

原始值 转换目标 结果
number 字符串 5=>'5'
Boolean、函数、Symbol 字符串 'true' "Symbol()" "function A(){}"
数组 字符串 [1,2]=>'1,2'
对象 字符串 '[object Object]'
原始值 转换目标 结果
string 数字 '1'=>1,'a'=>NaN
数组 数字 空数组为 0,存在一个元素且为数字转数字,其他情况 NaN
null 数字 0
除了数组的引用类型 、nudefined 数字 NaN
Symobl 数字 抛错

对象转原始类型

对象在转换类型的时候,会调用内置的[[ToPrimitive]]函数,对于函数来说,算法逻辑一般来说如下:

  • 如果已经是原始类型了,那就不需要转换了
  • 调用x.valueOf(),如果转换为基础类型,就返回转换的值
  • 调用x.toString(),如果转换为基础类型,就返回转换的值
  • 如果都没有返回原始类型,就会报错

当然你也可以重写Symbol.toPrimitive,该方法在转原始类型时调用优先级最高。

let a = {
  valueOf(){
    return 0
  },
  toString(){
    return '1'
  }
  [Symbol.toPrimitive](){
    return 2
  }
}
1 + a // =>3
1
2
3
4
5
6
7
8
9
10
11
12

# 1.4 运算

加法运算符不同于其他几个运算符,它有以下几个特点:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + "1" //'11'
true + true //2
4 + [1, 2, 3] //'41,2,3'
1
2
3

# 1.5 比较

1.如果是对象,就通过toPrimitive转换对象 2.如果是字符串,就通过unicode字符索引来比较

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return "1"
  },
}
a > 1 //false
1
2
3
4
5
6
7
8
9

在以上代码中,因为 A 是对象,所以通过valueOf转换为原始类型再比较值。

toString( ):返回对象的字符串表示。

//先看看toString()方法的结果
var a = 3
var b = "3"
var c = true
var d = { test: "123", example: 123 }
var e = function () {
  console.log("example")
}
var f = ["test", "example"]

a.toString() // "3"
b.toString() // "3"
c.toString() // "true"
d.toString() // "[object Object]"
e.toString() // "function (){console.log('example');}"
f.toString() // "test,example"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

valueOf( ):返回对象的字符串、数值或布尔值表示。

所有对象都有 valueof,如果存在任意原始值,他就默认将对象转化为表示它的原始值。如果对象是复合值,而却大部分对象无法真正表示一个原始值,因此默认的 valueof()方法简单的返回对象本身,而不是返回原始值。数组、函数、正则表达式简单的继承了这个 方法,返回对象本身

//再看看valueOf()方法的结果
var a = 3
var b = "3"
var c = true
var d = { test: "123", example: 123 }
var e = function () {
  console.log("example")
}
var f = ["test", "example"]

a.valueOf() // 3
b.valueOf() // "3"
c.valueOf() // true
d.valueOf() // {test:'123',example:123}
e.valueOf() // function(){console.log('example');}
f.valueOf() // ['test','example']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> undefined 和 null NaN
  • unll 类型也只有一个值,即 null null 用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象
  • NaN 与任何值都是比较结果都是 false
  • undefined 类型只要一个,即 undefined,当声明变量还未被初始化时就是 undefined,undefined 是一个表示"无"的原始值,转为数值时为   当声明的变量还未被初始化时,变量的默认值为 null 用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。
    • 变量被声明了,但没有赋值时,就等于 undefined。
    • 调用函数时,应该提供的参数没有提供,该参数等于 undefined。
    • 对象没有赋值的属性,该属性的值为 undefined。
    • 函数没有返回值时,默认返回 undefined。
    • 作为函数的参数,表示该函数的参数不是对象。
    • 作为对象原型链的终点。

# 1.6 相等

== 和 === 有什么区别?

对于==来说,如果对比双方的类型不一样的话,就会进行类型转换

假如我们需要对比xy是否相同,就会进行如下判断流程:

  • 1.首先会判断两者类型是否相同,相同的话就是比大小了
  • 2.类型不相同的话,那么就会进行类型转换
  • 3.首先会判断是否在比对nullundefined,是的话就会返回true
  • 4.判断两者类型是否为stringnumber,是的话就会将字符串转换为number
1 == "1" // 1 == 1
1

5.判断其中一方是否为boolean,是的话就会把boolean转为number再进行判断

"1" == true // '1' == 1 --> 1==1
1

6.判断其中一方是否为object且另一方为stringnumber或者symbol,是的话就会把object转为原始类型再进行判断

"1" == { name: "yck" } // '1' == '[object Object]'
1

# 1.7 拷贝

基本类型:undefined,null,Boolean,String,Number,Symbol 在内存中占据固定大小,保存在栈内存中

  • 基本数据类型的复制:其实就是创建一个新的副本给这个值赋值新的变量,改变旧值对象不会改变

引用数据类型:Object,Array,Date,Function,RegExp 等;引用数据类型的值是对象,保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。

  • 引用数据类型的复制:其实就是复制了指针,这个最终都将指向同一个对象,改变其新对象旧的值也会改变
  • 分为深浅拷贝两种,指在内存中的存储类型不同:
    • 浅拷贝:重新在堆栈中创建内存,拷贝前后对象的基本类型互不影响。只拷贝一层,不能对对象的子对象进行拷贝,slice concat object.assign ...
    • 深拷贝:对对象中的子对象进行递归拷贝,拷贝前后两个对象互不影响

深拷贝实现

  • 1.JSON.parse()将一个 js 对象序列化一个 json 字符串 JSON.stringify()将 json 字符串反序列化一个 js 对象
var obj = { key: { key: 1 } }
var obj2 = JSON.parse(JSON.stringify(obj))
1
2

局限性

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 不能拷贝函数

其他方式:MessageChannel、history、notification、递归、cloneDeep

参考资料

# 2.数组类型

# 2.1 数组方法

  • some: 有一项返回 true,则整体为 true

  • every: 有一项返回 false,则整体为 false

  • join: 通过指定连接符生成字符串

  • push / pop: 末尾推入和弹出,改变原数组, 返回推入/弹出项【有误】

  • unshift / shift: 头部推入和弹出,改变原数组,返回操作项【有误】

  • sort(fn) / reverse: 排序与反转,改变原数组

  • concat: 连接数组,不影响原数组, 浅拷贝

  • indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标

  • reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的 return 值,cur 为当前值(从第二项开始)

  • 1.slice(start,end)

    方法可以从已有数组中返回选定的元素,返回一个新数组,包含从 start 和 end(不包含该元素)的数组方法

  • 2.splice(start, number, value...)

    该方法向或者从从数组中添加或删除项目,返回被删除的项目。(该方法会改变原数组)

    • splice(index,howmany,iteml,...itemx)
    • index 参数;必须,整数规定添加或者删除的位置,使用负数,从数组尾部规定位置
    • howmany 参数:必须,要删除的数量
    • iteml..itemx:可选,向数组添加新项目
  • 3.map():

    会返回一个全新的数组。使用于改变数据值的时候。会分配内存存储空间数组并返回,forEach()不会返回数据

  • 4.forEach():

    • 不会返回任何有价值的东西,并且不打算改变数据,单纯的只是想用数据做一些事情,他允许 callback 更改原始数组的元素
    • 无法 break,可以用 try/catch 中 throw new Error 来停止
  • 5.reduce / reduceRight(fn(prev, cur), defaultPrev)

    方法接受一个函数作为累加器,数组中的每一个值(从左到右)开始缩减,最终计算一个值,不会改变原数组的值

  • 6.filter():

    方法创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,它里面通过 function 去做处理

# 2.2 数组去重

  • 1.双重循环
  • 2.indexOf
  • 3.数组排序去重 最快 以下是数组去重的三种方法:
Array.prototype.unique1 = function () {
  var n = [] //一个新的临时数组
  for (
    var i = 0;
    i < this.length;
    i++ //遍历当前数组
  ) {
    //如果当前数组的第 i 已经保存进了临时数组,那么跳过,
    //否则把当前项 push 到临时数组里面
    if (n.indexOf(this[i]) == -1) n.push(this[i])
  }
  return n
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Array.prototype.unique2 = function () {
  var n = {},
    r = [] //n 为 hash 表,r 为临时数组
  for (
    var i = 0;
    i < this.length;
    i++ //遍历当前数组
  ) {
    if (!n[this[i]]) {
      //如果 hash 表中没有当前项
      n[this[i]] = true //存入 hash 表
      r.push(this[i]) //把当前数组的当前项 push 到临时数组里面
    }
  }
  return r
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Array.prototype.unique3 = function () {
  var n = [this[0]] //结果数组
  for (
    var i = 1;
    i < this.length;
    i++ //从第二项开始遍历
  ) {
    //如果当前数组的第 i 项在当前数组中第一次出现的位置不是 i,
    //那么表示第 i 项是重复的,忽略掉。否则存入结果数组
    if (this.indexOf(this[i]) == i) n.push(this[i])
  }
  return n
}
1
2
3
4
5
6
7
8
9
10
11
12
13

类数组转换为数组的方法

  • Array.apply(null,arguments)
  • [...arguments]
  • Array.prototype.slice.apply(argumemts)
  • Array.form(arguements)

# 2.3 数组扁平化

  • ES6 的 flat()
const arr = [1, [2, 3], [5, 66, 7]]
arr.flat()
1
2
  • 序列化后正则
const arr = [1, [2, 3], [5, 66, 7]]
const str = `[${JSON.stringify(arr).replace(/(\[|\]\)/g, "")}]`
JSON.parse(str)
1
2
3
  • 递归
const arr = [1, [2, 3], [5, 66, 7]]
function flat(arr, data) {
  arr.map((item) => {
    if (Array.isArray(item)) {
      return flat(item, data)
    } else {
      data.push(item)
    }
  })
  return data
}
1
2
3
4
5
6
7
8
9
10
11
  • 迭代
const arr = [1, [2, 3], [5, 66, 7]]
while (arr.some(Array.isArray)) {
  arr = [].concat(...arr)
}
1
2
3
4

# 3.对象类型

# 4.函数类型

5.闭包

什么是闭包

  • 内部函数可以访问定义在他们外部函数的参数和变量。
  • 作用域链向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用执行完毕后销毁,设计私用的方法和变量,避免全局变量的污染
  • 嵌套函数的本质是将函数内部和外部连接起来,有点事可以读取函数内部的变量,让这些变量的值始终保持在内存中,不会再函数被调用之后自动清除

闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

function A() {
  let a = 1
  window.B = function () {
    console.log(a)
  }
}
A()
B()
1
2
3
4
5
6
7
8

很多人对于闭包的解释可能就是嵌套了函数,然后返回一个函数。其实这个解释是不完整的,就比如我上面这个例子就可以反驳这个观点。

闭包的缺陷

  • 常驻内存会增大内存的使用量
  • 使用不当会造成内存泄漏
  • 如果不是因为某些特殊的任务而需要使用闭包,在没有必要的情况下,在其他函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

# 3.1 原型

如何理解原型?如何理解原型链?

# 3.2 回调函数

什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

回调地狱的根本问题就是:

1.嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身

2.嵌套函数一多,就很难处理错误

当然回调函数还存在着别的几个缺点,比如不能用try catch捕获错误,不能直接return

# 8.并发和并行区别

并发与并行的区别

并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称为并发。

并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称为并行。

# 10.Generator

你理解的 Generator 是什么?

Generator最大的特点就是可以控制函数的执行。

# 11.Promise

Promise 的特点是什么,分别有什么缺点?什么是 Promise 链?Promise 构造函数执行和 then 函数执行有什么区别?

promise 有三种状态:pending、resolved、rejected 这个承诺一旦从等待状态变为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再改变

当我们在构造 promise 的时候,构造函数内部的代码是立即执行的

Promsie 实现了链式调用,很好地解决了回调地狱的问题,但是也有一些缺点比如无法取消 Promise,错误需要通过回调函数捕获。

promise

  • 1.是一个对象,用来传递异步操作的信息。代表着某个未才会知道结果的时间,并为这个事件提供统一的 api,供其进行异步处理
  • 2.有了这个对象,就可以让异步操作以同步的操作的流程来表达处理,避免层层嵌套的回调地狱
  • 3.promise 代表一个异步状态,有三个状态 pending(进行中),Resolve(已完成),reject(失败)
  • 4.一旦状态改变,就不会再变。任何时候都可以得到结果。从进行中变为已完成或失败
  • promise.all()里面的状态都改变,那就会输出,得到一个数组
  • promise.race()里面只有一个状态变为 rejected 或者 fulfilled 即输出
  • promise.finally()不管指定 pormise 对象最后的状态如何,都会执行的操作(本质上还是 then 方法的特例)

# 12.async 及 await

async 及 await 的特点,它们的优点和缺点分别是什么?await 原理是什么?

一个函数如果加上 async,那么这个函数就会返回一个 Promise

async 就是将函数返回值使用Promise.resolve()包裹了下,和then中处理返回值一样,并且await只能配套async

asyncawait可以说是异步终极解决方案了,相比直接使用 Promsie来说,优势在于处理then的调用链,能够更清晰准确的写出代码,毕竟写一大堆then也很恶心,并且也能优雅的解决回调地狱问题。当然也存在一些缺点。因为await将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await会导致性能上的降低。

其实await就是generator加上Promise的语法糖,且内部实现了自动执行generator。如果你熟系 co 的话,其实自己可以实现这样的语法糖。

# 13.常用定时器函数

setTimeout、setInterval、requestAnimationFrame 各有什么特点?

异步编程当然少不了定时器了,常见的定时器函数有setTimeoutsetIntervalrequsetAnimationFrame

因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致setTimeout不会按期执行。当然了,我们可以通过代码去修正setTimeout,从而使定时器相对准确。

接下来我们来看setInterval,其实这个函数作用和setTimeout基本一致,只是该函数是没隔一段时间执行一次回调函数。

通常来说不建议使用setInterval。第一,他和setTimeout一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题

如果你有循环定时器的需求,其实完全可以通过requestAnimationFrame来实现

首先requsetAnimationFrame自带函数节流的功能,基本可以保证在 16.6ms 内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现setTimeout

# 14.同步和异步

  • 同步:
    • 由于 js 单线程,同步任务都在主线程上排队执行,前面任务没有执行完成,后面的任务会一直等待
  • 异步:
    • 不进入主线程,进入任务队列,等待主线程任务执行完成,才开始执行。最基本的异步操作 setTimeout 和 setInterval,等待主线程任务执行完,在开始执行里面的函数。
    • 浏览器和 Node 环境下,microtask 任务队列的执行时机不同
    • Node.js 中,microtask 在事件循环的各个阶段之间执行
    • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
    • 递归的调用 process.nextTick()会导致 I/O starving,官方推荐使用 setImmediate()
上次更新: 2023/1/7 下午3:34:52