JavaScript要点精粹
Jacshuo Lv3

ES3

a=?, a=1 && a==2 && a==3成立

==会触发隐式转换而===不会

对象转字符串

  • 先尝试调用对象的 toString()
  • 对象无toString()toString返回非原始值,调用valueOf()方法
    • 将该值转为字符串,并返回字符串结果
  • 否则,抛出类型错误

对象转数字

  • 先尝试调用对象的valueOf(),将返回原始值转为数字
  • 对象无valueOf()valueOf返回不是原始值,调用toString()方法,将返回原始值转为数字
  • 否则,抛出类型错误

对象转布尔值

  • True

代码

1
2
3
4
5
6
const a = {
count: 0,
valueOf() {
return ++this.count
}
}

数组

隐式转换会调用数组的 join 方法,改写此方法

1
2
const a = [1, 2, 3]
a.join = a.shift

null == undefined 结果

比较 null 和 undefined 的时候,不能将 null 和 undefined 隐式转换,规范规定结果为相等。

常见的类型转换

类型 to Boolean to Number to String
Boolean true true 1 "true"
Boolean false false 0 "false"
Number 123 true 123 "123"
Number Infinity true Infinity "Infinity"
Number 0 false 0 "0"
Number NaN false NaN "NaN"
String "" false 0 ""
String "123" true 123 "123"
String "123abc" true NaN "123abc"
String "abc" true NaN "abc"
Null null false 0 "null"
Undefined undefined false NaN "undefined"
Function function() {} true NaN "function(){}"
Object {} true NaN "[object Object]"
Array [] true 0 ""
Array ["abc"] true NaN "abc"
Array ["123"] true 123 "123"
Array ["123", "abc"] true NaN "123, abc"

对比getObject.defineProperty

相同点

  • 都可以定义属性被查询时的函数

不同点

在 classes 内部使用

  • get 属性将被定义到实例原型
  • Object.defineProperty 属性将被定义在实例自身

对比escapeencodeURIencodeURIComponent

escape

对字符串编码
ASCII 字母、数字 @ * / + - _ . 之外的字符都被编码

encodeURI

对URL编码
ASCII 字母、数字 @ * / + 和 ~ ! # $ & () =, ; ?- _ . ‘之外的字符都被编码

encodeURIComponent

对URL编码
ASCII 字母、数字 ~ ! * ( ) - _ . ‘ 之外的字符都被编码

事件

事件传播的过程

事件冒泡

  • DOM0 和 IE支持(DOM1 开始是 W3C 规范)
  • 从事件源向父级,一直到根元素(HTML)
  • 某个元素的某类型事件被触发,父元素同类型事件也会被触发

事件捕获

  • DOM2 支持
  • 从根元素(HTML)到事件源
  • 某个元素的某类型事件被触发,先触发根元素,再向子一级,直到事件源

事件流

  • 事件的流向:事件捕获 → 事件源 → 事件冒泡

阻止事件冒泡

  • 标准:event.stopPropagation()
  • IE:event.cancelBubble = true

Event Loop的执行顺序

宏任务

  • Task Queue
  • 常见宏任务:setTimeoutsetIntervalsetImmediate、I/O、script、UI rendering

微任务

  • Job Queue
  • 常见微任务:
    • 浏览器:PromiseMutationObserver
    • Node.js:process.nextTick

执行顺序

  • 首先执行同步代码宏任务
  • 同步栈为空,查询是否有异步代码需要执行
  • 执行所有微任务
  • 执行完,是否需要渲染页面
  • 重新开始 Event Loop,执行宏任务中的异步代码

为什么Vue.$nextTick通常比setTimeout优先级高,渲染更快生效?

  • Vue.$nextTick 需要异步执行队列,异步函数的实现优先使用
    • Promise、MutationObserver、setImmediate
    • 都不兼容时,使用 setTimeout
  • Promise、MutationObserver、setImmediate 是微任务
  • setTimeout、UI rendering 是宏任务
  • 根据执行顺序
    • Promise、MutationObserver、setImmediate 创建微任务,添加到当前宏任务、微任务队列。
      队列任务执行完,如需渲染,即可渲染页面
    • setTimeout 创建宏任务,如果此时正在执行微任务队列,需要等队列执行完,渲染一次后,
      重新开始 Event Loop,执行宏任务中的异步代码后再渲染

ES6

ES6、ES7、ES8、ES9、ES10、ES11+ 新特性

ES6

  • letconst
  • Promise
  • Class
  • 箭头函数
  • 函数参数默认值
  • 模版字符串
  • 解构赋值
  • 展开语法
    • 构造数组,调用函数时,将 数组表达式 或 string 在语法层面展开
  • 对象属性缩写
    • 键名和键值相同
    • 函数省略 function
  • 模块化

ES7

  • includes()
  • 指数操作符 **

ES8

  • async / await
  • Object.values()
  • Object.entries()
  • Object.getOwnPropertyDescriptors()
  • 填充字符串到指定长度: padStart padEnd
  • ShareArrayBufferAtomics,共享内存位置读取和写入
  • 函数最后参数有 尾逗号,与 数组 和 对象 保持一致

ES9

  • 异步迭代: for await (let i of array)
  • Promise.prototype.finally()
  • 展开语法
    • 构造字面量对象时,将对象按照键值对展开,克隆属性或浅拷贝
  • 非转义序列的模版字符串
  • 正则表达式
    • 命名捕获组:
1
2
const match = /(?<year>\d{4})/.exec('2022')
console.log(match.groups.year) // 2022
  • 非转义序列的模版字符串
    • \u unicode转义
    • \x 十六进制转义
    • \ 后跟数字,八进制转义

ES10

  • JSON.stringify
    • \ud800\udfff单独转换,返回转义字符串
    • \ud800\udfff成对转换,对应字符存在,返回字符。不存在,返回转义字符串
  • flatflatMap
  • trimStarttrimEnd 去除字符串首尾空白字符
  • Object.fromEntries() 传入键值对列表,返回键值对对象
  • Symbol.prototype.description
1
2
const sym = Symbol('description')
sym.description // description
  • String.prototype.matchAll 返回包含所有匹配正则表达式和分组捕获结果的迭代器
  • Function.prototype.toString() 返回精确字符,包括空格和注释
  • 修改 catch 绑定
  • 新基本数据类型 BigInt
  • globalThis
  • import()

ES11+

  • String.prototype.replaceAll
  • Promise.any
    • 一个resolve返回第一个resolve状态
    • 所有reject返回请求失败
  • WeakRefs
    • 通过WeakMap WeakSet 创建
    • 创建对象的弱引用:该对象可被回收,即使它仍被引用
  • 逻辑运算符赋值表达式
    • ||=
      1
      a ||= b // 若 a 不存在,则 a = b
    • &&=
      1
      a &&= b // 若 a 存在,则 a = b
    • ??=
      1
      a ??= b // 若 a 为 null 或 undefined,则 a = b
    • ?.访问未定义属性
      1
      2
      const a = {}
      a?.b?.c // undefined,不报错
    • 数字分隔符
      1
      123_1569_9128 // 12315699128

== ===Object.is() 区别

== 相等,如果两边类型不一致,进行隐式转换后,再比较。+0 和 -0 相等, NaN 不等于任何数

=== 严格相等,如果类型不一致,就不相等。 +0 和 -0 相等, NaN 不等于任何数

Object.is() 严格相等,+0 和 -0 相等, NaN 等于自身

a = [],a.push(…[1, 2, 3]) ,a = ?

a = [1, 2, 3],考核点如下:

  • [].push:调用数组 push 方法
  • apply:
    • 第一参数:指定 push 执行时的 this,即正在调用 push 的对象为数组 a
    • 第二参数:传入数组或类数组对象作为参数数组,展开作为 push 的参数列表
  • push的语法:支持将一个或多个元素添加到数组末尾
1
arr.push(element1, ..., elementN)

综上,题目等同于

1
a.push(1, 2, 3) // [1, 2, 3]

变量

列举类数组对象

  • 定义
    拥有 length 属性
    若干索引属性的任意对象
  • 举例
    • NodeList
    • HTML Collections
  • 字符串
    • arguments
    • 返回的 jQuery 原型对象
  • 类数组对象转数组
    • 新建数组,遍历类数组对象,将每个元素放入新数组
    • Array.prototype.slice.call(ArrayLike) 或 [].slice.call(ArrayLike)
    • Array.from(ArrayLike)
    • apply 第二参数传入,调用数组方法
1
Array.prototype.push.apply([], ArrayLike)

闭包

什么是闭包?

闭包是由函数以及声明该函数的词法环境组合而成。

  • 一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。
  • 可以在内层函数中访问到其外层函数的作用域。
  • 每当创建一个函数,闭包就会在函数创建的同时被创建出来。

什么是词法?

词法,英文 lexical ,词法作用域根据源代码声明变量的位置来确定变量在何处可用嵌套函数可访问声明于它们外部作用域的变量

什么是函数柯里化?

函数调用:一个参数集合应用到函数。

部分应用:只传递部分参数,而非全部参数。

柯里化(curry):使函数理解并处理部分应用的过程。

保存调用curry函数时传入的参数,返回一个新函数。

结果函数在被调用后,要让新的参数和旧的参数一起应用到入参函数。

1
2
3
4
5
6
7
8
9
10
11
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}

bind / apply / call / new

手写 bind

  • 第一个参数接收 this 对象
  • 返回函数,根据使用方式
    • 直接调用
      • 改变 this 指向
      • 拼接参数
      • 调用函数
  • 构造函数
    • 不改变 this 指向,忽略第一参数
    • 拼接参数
    • new 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.myBind = function(_this, ...args) {
const fn = this
return function F(...args2) {
return this instanceof F ? new fn(...args, ...args2)
:fn.apply(_this, args.concat(args2))
}
}

//使用
function Sum(a, b) {
this.v = (this.v || 0) + a + b
return this
}

const NewSum = Sum.myBind({v: 1}, 2)
NewSum(3) // 调用:{v: 6}
new NewSum(3) // 构造函数:{v: 5} 忽略 myBind 绑定this

手写 call

  • 第一参数接收 this 对象
  • 改变 this 指向:将函数作为传入 this 对象的方法
  • 展开语法,支持传入和调用参数列表
  • 调用并删除方法,返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myCall = function(_this, ...args) {
if (!_this) _this = Object.create(null)
_this.fn = this
const res = _this.fn(...args)
delete _this.fn
return res
}

// 使用
function sum(a, b) {
return this.v + a + b
}

sum.myCall({v: 1}, 2, 3) // 6

手写 apply

  • 第一参数接收 this 对象
  • 改变 this 指向:将函数作为传入 this 对象的方法
  • 第二个参数默认数组
  • 展开语法,支持调用参数列表
  • 调用并删除方法,返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myApply = function(_this, args = []) {
if (!_this) _this = Object.create(null)
_this.fn = this
const res = _this.fn(...args)
delete _this.fn
return res
}

// 使用
function sum(a, b) {
return this.v + a + b
}

sum.myApply({v: 1}, [2, 3]) // 6

手写 new

  • 第一参数作为构造函数,其余参数作为构造函数参数
  • 继承构造函数原型创建新对象
  • 执行构造函数
  • 结果为对象,返回结果,反之,返回新对象
1
2
3
4
5
6
7
8
9
10
11
12
13
function myNew(...args) {
const Constructor = args[0]
const o = Object.create(Constructor.prototype)
const res = Constructor.apply(o, args.slice(1))
return res instanceof Object ? res:o
}

// 使用
function P(v) {
this.v = v
}

const p = myNew(P, 1) // P {v: 1}

手写防抖

  • 声明定时器
  • 返回函数
  • 一定时间间隔,执行回调函数
  • 回调函数
    • 已执行:清空定时器
    • 未执行:重置定时器
1
2
3
4
5
6
7
8
9
10
function debounce(fn, delay) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
fn.apply(this, args)
}, (delay + '') | 0 || 1000 / 60)
}
}

手写节流

  • 声明定时器
  • 返回函数
  • 一定时间间隔,执行回调函数
  • 回调函数
    • 已执行:清空定时器
    • 未执行:返回
1
2
3
4
5
6
7
8
9
10
function throttle(fn, interval) {
let timer = null
return function(...args) {
if (timer) return
timer = setTimeout(() => {
timer = null
fn.apply(this, args)
}, (interval + '') | 0 || 1000 / 60)
}
}

原型链

什么是原型链 ?

  1. 什么是原型对象,原型链 ?
  • JavaScript 是动态的,本身不提供一个 class 的实现。ES2015 / ES6 引入 class 关键字,只是语法糖,JavaScript
    仍然基于原型。
  • JavaScript 只有一种结构:对象 Object
    • 每个实例对象(Object)都有一个私有属性(称之为proto)指向它的构造函数的原型对象prototype)。
    • 该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为null
    • null没有原型,是这个原型链中的最后一个环节。
  • JavaScript 中的对象都是位于原型链顶端的Object的实例。
    综上,原型链就是从实例对象,指向原型对象,最终指向null的链接。
  1. 基于原型链的继承
  • 继承属性
    • JavaScript 对象是动态的属性“包”。
    • JavaScript 对象有一个指向一个原型对象的链。
    • 试图访问一个对象的属性时,不仅在该对象上搜寻,还会搜寻对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
    • 指向原型对象的和标准属性。
      • someObject.[[Prototype]] 符号是用于指向 someObject 的原型。
      • 从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf()
        Object.setPrototypeOf() 访问器来访问
      • 等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__
      • 不能与构造函数 func 的 prototype 属性相混淆。
        • 被构造函数创建的实例对象的 [[Prototype]] 指向 func 的 prototype 属性。
        • Object.prototype 属性表示 Object 的原型对象。
      • 除内置 gettersetter属性外,给对象设置属性会创建自有属性。
  • 继承方法
    • JavaScript没有其他基于类的语言所定义的“方法”。
    • JavaScript任何函数都可以添加到对象上作为对象的属性。
    • 函数继承与属性继承没有差别,包括“属性遮蔽”,即属性可被重写。
    • 当继承函数被调用时, this指向的是当前继承对象,而不是继承函数所在原型对象。

对比各种继承

  1. 原型链继承

子类原型指向父类实例

1
2
3
4
5
6
7
8
function Parent() {
}

function Child() {
}

Child.prototype = new Parent()
const child = new Child()

好处:

  • 子类可以访问到父类新增原型方法和属性

坏处:

  • 无法实现多继承
  • 创建实例不能传递参数
  1. 构造函数
1
2
3
4
5
6
7
8
function Parent() {
}

function Child(...args) {
Parent.call(this, ...args) // Parent.apply(this, args)
}

const child = new Child(1)

好处:

  • 可以实现多继承
  • 创建实例可以传递参数

坏处

  • 实例并不是父类的实例,姿势子类的实例
  • 不能集成副类型原型上的方法
  1. 组合继承
1
2
3
4
5
6
7
8
9
10
function Parent() {
}

function Child(...args) {
Parent.call(this, ...args)
}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
const child = new Child(1)

好处:

  • 属性和原型链上的方法都可以继承
  • 创建实例可以传递参数
  1. 对象继承
  • Object.create
1
2
3
4
5
const Parent = {
property: 'value', method: () => {
}
}
const Child = Object.create(Parent)
  • create
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Parent = {
property: 'value', method() {
}
}

function create(obj) {
function F() {
}

F.prototype = obj
return new F()
}

const child = create(Parent)

好处:

  • 可以继承属性和方法

坏处:

  • 创建实例无法传递参数
  • 传入对象的属性有引用类型,所有类型都会共享相应的值
  1. 寄生组合继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Parent() {
}

function Child(...args) {
Parent.call(this, args)
}

function create(obj) {
function F() {
}

F.prototype = obj
return F
}

function extend(Child, Parent) {
const clone = create(Parent.prototype)
clone.constructor = Child
Child.prototype = clone
}

extend(Child, Parent)
const child = new Child(1)

  1. ES6继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent {
constructor(property) {
this.property = property
}

method() {
}
}

class Child extends Parent {
constructor(property) {
super(property)
}
}

什么是作用域?

作用域即代码中的变量、函数和对象的作用范围,变量、函数和对象只在这个范围内有效,可以被访问,超出范围失效或不可见。

作用域可以提高程序逻辑的局部性、增强可靠性,减少命名冲突。

ES6之前,Javascript有:

  • 全局作用域
    • 最外层变量、函数和对象拥有全局作用域。
    • 全局对象,例如global,window等拥有全局作用域。
    • ES5以下以及ES5非严格模式,函数内未定义的变量,自动挂载在全局对象下,拥有全局作用域
  • 函数作用域
    • 函数内部的变量、函数和对象拥有函数作用域,仅在函数内部可以被访问。
    • 使用var声明的变量和函数,如果不在作用域顶部,会进行变量提升,初始值为undefined

使用function声明的函数,如果不在作用域顶部,会进行函数提升,函数声明及函数体都会提升到顶部。

ES6及之后,JavaScript增加:

  • 块级作用域,即花括号包裹的内部。使用letconst声明的变量和函数,不会变量提升,存在暂时性死区,即不能不声明直接使用,仅在块级
    作用域下可用,统一作用域下,不可重复声明。
  • 作用域链,外层作用域不可访问内层作用域,内从作用域可以访问外层作用域。
    如果内层作用域使用的变量、函数、对象在内层和外层作用域都找不到,会继续向外层作用域的更外层作用域寻找,直到找到或到最外层为止,这便是作用域链。

模块化

对比import,import()require

import import() require
规范 ES6Module ES6Module CommonJS
执行阶段 静态 编译阶段 动态 执行阶段 动态 执行阶段
顺序 置顶最先 异步 同步
缓存
默认导出 default default 直接赋值
导入赋值 解构赋值,传递引用 在then方法中解构赋值,属性值是仅可读,不可修改 基础类型 赋值,引用类型 浅拷贝

如何实现一个深拷贝?

JSON大法

1
2
3
function deepCopy(o) {
return JSON.parse(JSON.stringify(o))
}
  • 不支持Symbol, BigInt, Function类型。
  • 不支持循环引用。
  • 丢失值为undefined的键。

递归1

  • 递归处理引用类型
  • 保持数组类型
1
2
3
4
5
6
7
8
9
10
11
function deepCopy(target) {
if (typeof target === 'object') {
const newTarget = Array.isArray(target) ? []:Object.create(null)
for (const key in target) {
newTarget[key] = deepCopy(target[key])
}
return newTarget
} else {
return target
}
}

递归2

  • 哈希表Map支持循环引用。
  • Map支持引用类型数据作为键。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

function deepCopy(target, h = new Map) {
if (typeof target === 'object') {
if (h.has(target)) {
return h.get(target)
}
const newTarget = Array.isArray(target) ? []:Object.create(null)
for (const key in target) {
newTarget[key] = deepCopy(target[key], h)
}
h.set(target, newTarget)
return newTarget
} else {
return target
}
}

递归3

  • 哈希表WeakMap代替Map
  • WeakMap的键是弱引用,告诉JS垃圾回收机制,当键回收时,对应WeakMap也可以回收,更适合大量数据深拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepCopy(target, h = new WeakMap) {
if (typeof target === 'object') {
if (h.has(target)) return h.get(target)
const newTarget = Array.isArray(target) ? []:Object.create(null)
for (const key in target) {
newTarget[key] = deepCopy(target[key], h)
}
h.set(target, newTarget)
return newTarget
} else {
return target
}
}

可继续完善点

  • 递归改为迭代,预防栈溢出。
  • 支持null、Symbol、BigInt、布尔对象、正则对象、Data对象等深拷贝。
  • 使用while/for代替遍历数组,使用Object.keys()代替遍历对象。

0.1+0.2!==0.3的原因,如何解决?

计算机采用二进制表示十进制,将十进制的0.1转为二进制:

1
2
3
4
5
6
7
0.1 * 2 = 0.2 0
0.2 * 2 = 0.4 0
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 0
0.6 * 2 = 1.2 1
0.2 * 2 = 0.4 0
...

用科学计数法表示:2 ^ -4 * 1.10011(0011),将十进制的0.2转为二进制

1
2
3
4
5
6
0.2 * 2 = 0.4 0
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 1
0.6 * 2 = 1.2 1
0.2 * 2 = 0.4 0
...

用科学计数法表示:2 ^ -3 * 1.10011(0011)

JS 采用 IEEE 754 双精度版本(64位):

  • 64位 = 符号位(1位) + 整数(11位) + 小数(52位)
  • 小数超出 52位,四舍五入
    • 0.1:2 ^ -4 * 1.10011(0011 * 12)01
    • 0.2:2 ^ -3 * 1.10011(0011 * 12)010
    • 0.1 + 0.2:2 ^ -3 后面部分相加
1
2
3
4
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
—————————————————————————————————————————————————————————
= 10.0110011001100110011001100110011001100110011001100111

超出52位,四舍五入:2 ^ -2 * 1.0011(0011 * 12)01

转换为十进制:2 ^ -2 + 2 ^ -5 + 2 ^ -6 + 2 ^ -9 + 2 ^ -10 + 2 ^ -13 + 2 ^ -14 + … + 2
^ -49 + 2 ^ -50 + 2 ^ -52

1
2
3
let q = -2, sum = 2 ** -2
while ((q -= 3) >= -50) sum += 2 ** q + 2 ** --q
sum += 2 ** -52 // 0.30000000000000004

2 ^ -2 * 1.0011(0011 * 12)01 = 0.30000000000000004 > 0.3

如何解决此问题?

  • Number.EPSILON
    • 表示 1 与 Number 间可表示的大于 1 的最小的浮点数之间的差值
    • 接近于 Math.max(2, -52),Number.EPSILON !== Math.max(2, -52)
1
2
3
const equal = (a, b, c) => {
return Math.abs(c - a - b) < Number.EPSILON
}
  • toFixed(digits) digits 属于 0 到 20 间,默认 0,实际支持更大范围
    • 使用定点表示法来格式化一个数值,返回给定数字的字符串
1
2
3
const equal = (a, b, c) => {// 产生 0.1 + 0.2 === 0.333 结果
return (a + b).toFixed(1) === c.toFixed(1)
}
  • 小数转整数运算
1
2
3
4
5
const sum = (a, b) => {
const len = Math.max((a + '').split('.')[1].length,
(b + '').split('.')[1].length)
return (a * len + b * len) / len
}
  • 使用bignumber.js、bigdecimal.js等JS库。
  • 本文标题:JavaScript要点精粹
  • 本文作者:Jacshuo
  • 创建时间:2022-03-17 20:33:41
  • 本文链接:https://blog.imade.life/2022/03/17/JavaScript精粹/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!