ES6 特性

前言

本文的知识点完全是参考或摘录《ES6》里的语句,有部分语句为了方便理解和记忆,进行了相同意思的转义,同时对知识点进行归类划分。为了让大家能集中精力来记住这些特性,全文一句废话和题外话都没有,全部模块以笔记的形式进行书写,如果看得不是很惯建议对照《ES6》的内容来学习。

1
以下提到的《ECMAScript 6 入门》统一使用《ES6》这个名称来代替,而最新的ES6版本也是截止到当前的ES2019

本文整理出来的笔记都是书中的精华内容,囊括了整个ES6体系的所有特性,非常方便大家重新认识全部ES6特性。半小时的阅读就可以对ES6有一个全面的了解,可以认为是一本ES6特性小字典

img

修正

ES6ECMAJavaScript制定的第6个标准版本,相关历史可查看此章节《ES6-ECMAScript6简介》。

标准委员会最终决定,标准在每年6月正式发布并作为当年的正式版本,接下来的时间里就在此版本的基础上进行改动,直到下一年6月草案就自然变成新一年的版本,这样一来就无需以前的版本号,只要用年份标记即可。ECMAscript 2015是在2015年6月发布ES6的第一个版本。以此类推,ECMAscript 2016是ES6的第二个版本、 ECMAscript 2017是ES6的第三个版本。ES6既是一个历史名词也是一个泛指,含义是5.1版本以后的JavaScript下一代标准,目前涵盖了ES2015ES2016ES2017ES2018ES2019

所以有些文章上提到的ES7(实质上是ES2016)、ES8(实质上是ES2017)、ES9(实质上是ES2018)、ES10(实质上是ES2019),实质上都是一些不规范的概念。从ES1到ES6,每个标准都是花了好几年甚至十多年才制定下来,你一个ES6到ES7,ES7到ES8,才用了一年,按照这样的定义下去,那不是很快就ES20了。用正确的概念来说ES6目前涵盖了ES2015(ES6.0)、ES2016(ES6.1)、ES2017(ES6.2)、ES2018(ES6.3)、ES2019(ES6.4)。

另外,ES6更新的内容主要分为以下几点

  • 表达式:声明、解构赋值
  • 内置对象:字符串扩展、数值扩展、对象扩展、数组扩展、函数扩展、正则扩展、Symbol、Set、Map、Proxy、Reflect
  • 语句与运算:Class、Module、Iterator
  • 异步编程:Promise、Generator、Async

ES2015

声明

  • const命令:声明常量
  • let命令:声明变量

作用

  • 作用域

    • 全局作用域
    • 函数作用域function() {}
    • 块级作用域{}
  • 作用范围

    • var在全局代码中执行
    • constlet只能在代码块中执行
  • 赋值使用

    • const声明常量后必须立马赋值
    • let声明变量后可立马赋值或使用时赋值
  • 声明方法:varconstletfunctionclassimport

重点难点

  • 不允许重复声明
  • 未定义就使用会报错:constlet不存在变量提升
  • 暂时性死区:只要块级作用域内存在constlet,所声明常量和变量就绑定此区域,不再受外部影响

解构赋值

  • 字符串解构const [a, b, c, d, e] = "hello"

  • 数值解构const { toString: s } = 123

  • 布尔值解构const { toString: b } = true

  • 对象解构

    • 形式:const { x, y } = { x: 1, y: 2 }
    • 默认:const { x, y = 2 } = { x: 1 }
    • 改名:const { x, y: z } = { x: 1, y: 2 }
  • 数组解构

    • 规则:数据结构具有Iterator接口可采用数组形式的解构赋值
    • 形式:const [x, y] = [1, 2]
    • 默认:const [x, y = 2] = [1]
  • 函数参数解构

    • 数组解构:function Func([x = 0, y = 1]) {}
    • 对象解构:function Func({ x = 0, y = 1 } = {}) {}

应用场景

  • 交换变量值:[x, y] = [y, x]
  • 返回函数多个值:const [x, y, z] = Func()
  • 定义函数参数:Func([1, 2])
  • 提取JSON数据:const { name, version } = packageJson
  • 定义函数参数默认值:function Func({ x = 1, y = 2 } = {}) {}
  • 遍历Map结构:for (let [k, v] of Map) {}
  • 输入模块指定属性和方法:const { readFile, writeFile } = require("fs")

重点难点

  • 匹配模式:只要等号两边的模式相同,左边的变量就会被赋予对应的值
  • 解构赋值规则:只要等号右边的值不是对象或数组,就先将其转为对象
  • 解构默认值生效条件:属性值严格等于undefined
  • 解构遵循匹配模式
  • 解构不成功时变量的值等于undefined
  • undefinednull无法转为对象,因此无法进行解构

字符串扩展

  • Unicode表示法大括号包含表示Unicode字符(\u{0xXX}\u{0XXX})
  • 字符串遍历:可通过for-of遍历字符串
  • 字符串模板:可单行可多行可插入变量的增强版字符串
  • 标签模板:函数参数的特殊调用
  • String.raw():返回把字符串所有变量替换且对斜杠进行转义的结果
  • String.fromCodePoint():返回码点对应字符
  • codePointAt():返回字符对应码点(String.fromCodePoint()的逆操作)
  • normalize():把字符的不同表示方法统一为同样形式,返回新字符串(Unicode正规化)
  • repeat():把字符串重复n次,返回新字符串
  • matchAll():返回正则表达式在字符串的所有匹配
  • includes():是否存在指定字符串
  • startsWith():是否存在字符串头部指定字符串
  • endsWith():是否存在字符串尾部指定字符串

重点难点

  • 以上扩展方法均可作用于由4个字节储存Unicode字符

数值扩展

  • 二进制表示法0b或0B开头表示二进制(0bXX0BXX)
  • 八进制表示法0o或0O开头表示二进制(0oXX0OXX)
  • Number.EPSILON:数值最小精度
  • Number.MIN_SAFE_INTEGER:最小安全数值(-2^53)
  • Number.MAX_SAFE_INTEGER:最大安全数值(2^53)
  • Number.parseInt():返回转换值的整数部分
  • Number.parseFloat():返回转换值的浮点数部分
  • Number.isFinite():是否为有限数值
  • Number.isNaN():是否为NaN
  • Number.isInteger():是否为整数
  • Number.isSafeInteger():是否在数值安全范围内
  • Math.trunc():返回数值整数部分
  • Math.sign():返回数值类型(正数1负数-1零0)
  • Math.cbrt():返回数值立方根
  • Math.clz32():返回数值的32位无符号整数形式
  • Math.imul():返回两个数值相乘
  • Math.fround():返回数值的32位单精度浮点数形式
  • Math.hypot():返回所有数值平方和的平方根
  • Math.expm1():返回e^n - 1
  • Math.log1p():返回1 + n的自然对数(Math.log(1 + n))
  • Math.log10():返回以10为底的n的对数
  • Math.log2():返回以2为底的n的对数
  • Math.sinh():返回n的双曲正弦
  • Math.cosh():返回n的双曲余弦
  • Math.tanh():返回n的双曲正切
  • Math.asinh():返回n的反双曲正弦
  • Math.acosh():返回n的反双曲余弦
  • Math.atanh():返回n的反双曲正切

对象扩展

  • 简洁表示法:直接写入变量和函数作为对象的属性和方法({ prop, method() {} })

  • 属性名表达式:字面量定义对象时使用[]定义键([prop],不能与上同时使用)

  • 方法的name属性:返回方法函数名

    • 取值函数(getter)和存值函数(setter):get/set 函数名(属性的描述对象在getset上)
    • bind返回的函数:bound 函数名
    • Function构造函数返回的函数实例:anonymous
  • 属性的可枚举性和遍历:描述对象的enumerable

  • super关键字:指向当前对象的原型对象(只能用在对象的简写方法中method() {})

  • Object.is():对比两值是否相等

  • Object.assign():合并对象(浅拷贝),返回原对象

  • Object.getPrototypeOf():返回对象的原型对象

  • Object.setPrototypeOf():设置对象的原型对象

  • proto:返回或设置对象的原型对象

属性遍历

  • 描述:自身可继承可枚举非枚举Symbol

  • 遍历

    • for-in:遍历对象自身可继承可枚举属性
    • Object.keys():返回对象自身可枚举属性的键组成的数组
    • Object.getOwnPropertyNames():返回对象自身可继承可枚举非枚举属性的键组成的数组
    • Object.getOwnPropertySymbols():返回对象Symbol属性的键组成的数组
    • Reflect.ownKeys():返回对象自身可继承可枚举非枚举Symbol属性的键组成的数组
  • 规则

    • 首先遍历所有数值键,按照数值升序排列
    • 其次遍历所有字符串键,按照加入时间升序排列
    • 最后遍历所有Symbol键,按照加入时间升序排列

数组扩展

  • 扩展运算符(…):转换数组为用逗号分隔的参数序列([...arr],相当于rest/spread参数的逆运算)

  • Array.from():转换具有Iterator接口的数据结构为真正数组,返回新数组

    • 类数组对象:包含length的对象Arguments对象NodeList对象
    • 可遍历对象:StringSet结构Map结构Generator函数
  • Array.of():转换一组值为真正数组,返回新数组

  • copyWithin():把指定位置的成员复制到其他位置,返回原数组

  • find():返回第一个符合条件的成员

  • findIndex():返回第一个符合条件的成员索引值

  • fill():根据指定值填充整个数组,返回原数组

  • keys():返回以索引值为遍历器的对象

  • values():返回以属性值为遍历器的对象

  • entries():返回以索引值和属性值为遍历器的对象

  • 数组空位:ES6明确将数组空位转为undefined(空位处理规不一,建议避免出现)

扩展应用

  • 克隆数组:const arr = [...arr1]
  • 合并数组:const arr = [...arr1, ...arr2]
  • 拼接数组:arr.push(...arr1)
  • 代替apply:Math.max.apply(null, [x, y]) => Math.max(...[x, y])
  • 转换字符串为数组:[..."hello"]
  • 转换类数组对象为数组:[...Arguments, ...NodeList]
  • 转换可遍历对象为数组:[...String, ...Set, ...Map, ...Generator]
  • 与数组解构赋值结合:const [x, ...rest/spread] = [1, 2, 3]
  • 计算Unicode字符长度:Array.from("hello").length => [..."hello"].length

重点难点

  • 使用keys()values()entries()返回的遍历器对象,可用for-of自动遍历或next()手动遍历

函数扩展

  • 参数默认值:为函数参数指定默认值

    • 指定某个参数不得省略,省略即抛出错误:function Func(x = throwMissing()) {}
    • 将参数默认值设为undefined,表明此参数可省略:Func(undefined, 1)
    • 形式:function Func(x = 1, y = 2) {}
    • 参数赋值:惰性求值(函数调用后才求值)
    • 参数位置:尾参数
    • 参数作用域:函数作用域
    • 声明方式:默认声明,不能用constlet再次声明
    • length:返回没有指定默认值的参数个数
    • 与解构赋值默认值结合:function Func({ x = 1, y = 2 } = {}) {}
    • 应用
  • rest/spread参数(…):返回函数多余参数

    • 形式:以数组的形式存在,之后不能再有其他参数
    • 作用:代替Arguments对象
    • length:返回没有指定默认值的参数个数但不包括rest/spread参数
  • 严格模式:在严格条件下运行JS

    • 应用:只要函数参数使用默认值、解构赋值、扩展运算符,那么函数内部就不能显式设定为严格模式
  • name属性:返回函数的函数名

    • 将匿名函数赋值给变量:空字符串(ES5)、变量名(ES6)
    • 将具名函数赋值给变量:函数名(ES5和ES6)
    • bind返回的函数:bound 函数名(ES5和ES6)
    • Function构造函数返回的函数实例:anonymous(ES5和ES6)
  • 箭头函数(=>):函数简写

    • 并非因为内部有绑定this的机制,而是根本没有自己的this,导致内部的this就是外层代码块的this
    • 因为没有this,因此不能用作构造函数
    • 无参数:() => {}
    • 单个参数:x => {}
    • 多个参数:(x, y) => {}
    • 解构参数:({x, y}) => {}
    • 嵌套使用:部署管道机制
    • this指向固定化
  • 尾调用优化:只保留内层函数的调用帧

    • 定义:函数尾调用自身
    • 作用:只要使用尾递归就不会发生栈溢出,相对节省内存
    • 实现:把所有用到的内部变量改写成函数的参数并使用参数默认值
    • 定义:某个函数的最后一步是调用另一个函数
    • 形式:function f(x) { return g(x); }
    • 尾调用
    • 尾递归

箭头函数误区

  • 函数体内的this定义时所在的对象而不是使用时所在的对象
  • 可让this指向固定化,这种特性很有利于封装回调函数
  • 不可当作构造函数,因此箭头函数不可使用new命令
  • 不可使用yield命令,因此箭头函数不能用作Generator函数
  • 不可使用Arguments对象,此对象在函数体内不存在(可用rest/spread参数代替)
  • 返回对象时必须在对象外面加上括号

正则扩展

  • 变更RegExp构造函数入参:允许首参数为正则对象,尾参数为正则修饰符(返回的正则表达式会忽略原正则表达式的修饰符)

  • 正则方法调用变更:字符串对象的match()replace()search()split()内部调用转为调用RegExp实例对应的RegExp.prototype[Symbol.方法]

  • u修饰符:Unicode模式修饰符,正确处理大于\uFFFFUnicode字符

    • 点字符(.)
    • Unicode表示法
    • 量词
    • 预定义模式
    • i修饰符
    • 转义
  • y修饰符:粘连修饰符,确保匹配必须从剩余的第一个位置开始全局匹配(与g修饰符作用类似)

  • unicode:是否设置u修饰符

  • sticky:是否设置y修饰符

  • flags:正则表达式的修饰符

重点难点

  • y修饰符隐含头部匹配标志^
  • 单单一个y修饰符match()只能返回第一个匹配,必须与g修饰符联用才能返回所有匹配

Symbol

  • 定义:独一无二的值

  • 声明:const set = Symbol(str)

  • 入参:字符串(可选)

  • 方法

    • Symbol():创建以参数作为描述的Symbol值(不登记在全局环境)
    • Symbol.for():创建以参数作为描述的Symbol值,如存在此参数则返回原有的Symbol值(先搜索后创建,登记在全局环境)
    • Symbol.keyFor():返回已登记的Symbol值的描述(只能返回Symbol.for()key)
    • Object.getOwnPropertySymbols():返回对象中所有用作属性名的Symbol值的数组
  • 内置

    • Symbol.hasInstance:指向一个内部方法,当其他对象使用instanceof运算符判断是否为此对象的实例时会调用此方法
    • Symbol.isConcatSpreadable:指向一个布尔值,定义对象用于Array.prototype.concat()时是否可展开
    • Symbol.species:指向一个构造函数,当实例对象使用自身构造函数时会调用指定的构造函数
    • Symbol.match:指向一个函数,当实例对象被String.prototype.match()调用时会重新定义match()的行为
    • Symbol.replace:指向一个函数,当实例对象被String.prototype.replace()调用时会重新定义replace()的行为
    • Symbol.search:指向一个函数,当实例对象被String.prototype.search()调用时会重新定义search()的行为
    • Symbol.split:指向一个函数,当实例对象被String.prototype.split()调用时会重新定义split()的行为
    • Symbol.iterator:指向一个默认遍历器方法,当实例对象执行for-of时会调用指定的默认遍历器
    • Symbol.toPrimitive:指向一个函数,当实例对象被转为原始类型的值时会返回此对象对应的原始类型值
    • Symbol.toStringTag:指向一个函数,当实例对象被Object.prototype.toString()调用时其返回值会出现在toString()返回的字符串之中表示对象的类型
    • Symbol.unscopables:指向一个对象,指定使用with时哪些属性会被with环境排除

数据类型

  • Undefined
  • Null
  • String
  • Number
  • Boolean
  • Object(包含ArrayFunctionDateRegExpError)
  • Symbol

应用场景

  • 唯一化对象属性名:属性名属于Symbol类型,就都是独一无二的,可保证不会与其他属性名产生冲突
  • 消除魔术字符串:在代码中多次出现且与代码形成强耦合的某一个具体的字符串或数值
  • 遍历属性名:无法通过for-infor-ofObject.keys()Object.getOwnPropertyNames()JSON.stringify()返回,只能通过Object.getOwnPropertySymbols返回
  • 启用模块的Singleton模式:调用一个类在任何时候返回同一个实例(windowglobal),使用Symbol.for()来模拟全局的Singleton模式

重点难点

  • Symbol()生成一个原始类型的值不是对象,因此Symbol()前不能使用new命令
  • Symbol()参数表示对当前Symbol值的描述,相同参数的Symbol()返回值不相等
  • Symbol值不能与其他类型的值进行运算
  • Symbol值可通过String()toString()显式转为字符串
  • Symbol值作为对象属性名时,此属性是公开属性,但不是私有属性
  • Symbol值作为对象属性名时,只能用方括号运算符([])读取,不能用点运算符(.)读取
  • Symbol值作为对象属性名时,不会被常规方法遍历得到,可利用此特性为对象定义非私有但又只用于内部的方法

Set

Set
  • 定义:类似于数组的数据结构,成员值都是唯一且没有重复的值

  • 声明:const set = new Set(arr)

  • 入参:具有Iterator接口的数据结构

  • 属性

    • constructor:构造函数,返回Set
    • size:返回实例成员总数
  • 方法

    • add():添加值,返回实例
    • delete():删除值,返回布尔值
    • has():检查值,返回布尔值
    • clear():清除所有成员
    • keys():返回以属性值为遍历器的对象
    • values():返回以属性值为遍历器的对象
    • entries():返回以属性值和属性值为遍历器的对象
    • forEach():使用回调函数遍历每个成员

应用场景

  • 去重字符串:[...new Set(str)].join("")

  • 去重数组:[...new Set(arr)]Array.from(new Set(arr))

  • 集合数组

    • 声明:const a = new Set(arr1)const b = new Set(arr2)
    • 并集:new Set([...a, ...b])
    • 交集:new Set([...a].filter(v => b.has(v)))
    • 差集:new Set([...a].filter(v => !b.has(v)))
  • 映射集合

    • 声明:let set = new Set(arr)
    • 映射:set = new Set([...set].map(v => v * 2))set = new Set(Array.from(set, v => v * 2))

重点难点

  • 遍历顺序:插入顺序
  • 没有键只有值,可认为键和值两值相等
  • 添加多个NaN时,只会存在一个NaN
  • 添加相同的对象时,会认为是不同的对象
  • 添加值时不会发生类型转换(5 !== "5")
  • keys()values()的行为完全一致,entries()返回的遍历器同时包括键和值且两值相等
WeakSet
  • 定义:和Set结构类似,成员值只能是对象

  • 声明:const set = new WeakSet(arr)

  • 入参:具有Iterator接口的数据结构

  • 属性

    • constructor:构造函数,返回WeakSet
  • 方法

    • add():添加值,返回实例
    • delete():删除值,返回布尔值
    • has():检查值,返回布尔值

应用场景

  • 储存DOM节点:DOM节点被移除时自动释放此成员,不用担心这些节点从文档移除时会引发内存泄漏
  • 临时存放一组对象或存放跟对象绑定的信息:只要这些对象在外部消失,它在WeakSet结构中的引用就会自动消

重点难点

  • 成员都是弱引用,垃圾回收机制不考虑WeakSet结构对此成员的引用
  • 成员不适合引用,它会随时消失,因此ES6规定WeakSet结构不可遍历
  • 其他对象不再引用成员时,垃圾回收机制会自动回收此成员所占用的内存,不考虑此成员是否还存在于WeakSet结构

Map

Map
  • 定义:类似于对象的数据结构,成员键可以是任何类型的值

  • 声明:const set = new Map(arr)

  • 入参:具有Iterator接口且每个成员都是一个双元素数组的数据结构

  • 属性

    • constructor:构造函数,返回Map
    • size:返回实例成员总数
  • 方法

    • get():返回键值对
    • set():添加键值对,返回实例
    • delete():删除键值对,返回布尔值
    • has():检查键值对,返回布尔值
    • clear():清除所有成员
    • keys():返回以键为遍历器的对象
    • values():返回以值为遍历器的对象
    • entries():返回以键和值为遍历器的对象
    • forEach():使用回调函数遍历每个成员

重点难点

  • 遍历顺序:插入顺序
  • 对同一个键多次赋值,后面的值将覆盖前面的值
  • 对同一个对象的引用,被视为一个键
  • 对同样值的两个实例,被视为两个键
  • 键跟内存地址绑定,只要内存地址不一样就视为两个键
  • 添加多个以NaN作为键时,只会存在一个以NaN作为键的值
  • Object结构提供字符串—值的对应,Map结构提供值—值的对应
WeakMap
  • 定义:和Map结构类似,成员键只能是对象

  • 声明:const set = new WeakMap(arr)

  • 入参:具有Iterator接口且每个成员都是一个双元素数组的数据结构

  • 属性

    • constructor:构造函数,返回WeakMap
  • 方法

    • get():返回键值对
    • set():添加键值对,返回实例
    • delete():删除键值对,返回布尔值
    • has():检查键值对,返回布尔值

应用场景

  • 储存DOM节点:DOM节点被移除时自动释放此成员键,不用担心这些节点从文档移除时会引发内存泄漏
  • 部署私有属性:内部属性是实例的弱引用,删除实例时它们也随之消失,不会造成内存泄漏

重点难点

  • 成员键都是弱引用,垃圾回收机制不考虑WeakMap结构对此成员键的引用
  • 成员键不适合引用,它会随时消失,因此ES6规定WeakMap结构不可遍历
  • 其他对象不再引用成员键时,垃圾回收机制会自动回收此成员所占用的内存,不考虑此成员是否还存在于WeakMap结构
  • 一旦不再需要,成员会自动消失,不用手动删除引用
  • 弱引用的只是键而不是值,值依然是正常引用
  • 即使在外部消除了成员键的引用,内部的成员值依然存在

Proxy

  • 定义:修改某些操作的默认行为

  • 声明:const proxy = new Proxy(target, handler)

  • 入参

    • target:拦截的目标对象
    • handler:定制拦截行为
  • 方法

    • Proxy.revocable():返回可取消的Proxy实例(返回{ proxy, revoke },通过revoke()取消代理)
  • 拦截方式

    • get():拦截对象属性读取
    • set():拦截对象属性设置,返回布尔值
    • has():拦截对象属性检查k in obj,返回布尔值
    • deleteProperty():拦截对象属性删除delete obj[k],返回布尔值
    • defineProperty():拦截对象属性定义Object.defineProperty()Object.defineProperties(),返回布尔值
    • ownKeys():拦截对象属性遍历for-inObject.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols(),返回数组
    • getOwnPropertyDescriptor():拦截对象属性描述读取Object.getOwnPropertyDescriptor(),返回对象
    • getPrototypeOf():拦截对象原型读取instanceofObject.getPrototypeOf()Object.prototype.__proto__Object.prototype.isPrototypeOf()Reflect.getPrototypeOf(),返回对象
    • setPrototypeOf():拦截对象原型设置Object.setPrototypeOf(),返回布尔值
    • isExtensible():拦截对象是否可扩展读取Object.isExtensible(),返回布尔值
    • preventExtensions():拦截对象不可扩展设置Object.preventExtensions(),返回布尔值
    • apply():拦截Proxy实例作为函数调用proxy()proxy.apply()proxy.call()
    • construct():拦截Proxy实例作为构造函数调用new proxy()

应用场景

  • Proxy.revocable():不允许直接访问对象,必须通过代理访问,一旦访问结束就收回代理权不允许再次访问
  • get():读取未知属性报错、读取数组负数索引的值、封装链式操作、生成DOM嵌套节点
  • set():数据绑定(Vue数据绑定实现原理)、确保属性值设置符合要求、防止内部属性被外部读写
  • has():隐藏内部属性不被发现、排除不符合属性条件的对象
  • deleteProperty():保护内部属性不被删除
  • defineProperty():阻止属性被外部定义
  • ownKeys():保护内部属性不被遍历

重点难点

  • 要使Proxy起作用,必须针对实例进行操作,而不是针对目标对象进行操作
  • 没有设置任何拦截时,等同于直接通向原对象
  • 属性被定义为不可读写/扩展/配置/枚举时,使用拦截方法会报错
  • 代理下的目标对象,内部this指向Proxy代理

Reflect

  • 定义:保持Object方法的默认行为

  • 方法

    • get():返回对象属性
    • set():设置对象属性,返回布尔值
    • has():检查对象属性,返回布尔值
    • deleteProperty():删除对象属性,返回布尔值
    • defineProperty():定义对象属性,返回布尔值
    • ownKeys():遍历对象属性,返回数组(Object.getOwnPropertyNames()+Object.getOwnPropertySymbols())
    • getOwnPropertyDescriptor():返回对象属性描述,返回对象
    • getPrototypeOf():返回对象原型,返回对象
    • setPrototypeOf():设置对象原型,返回布尔值
    • isExtensible():返回对象是否可扩展,返回布尔值
    • preventExtensions():设置对象不可扩展,返回布尔值
    • apply():绑定this后执行指定函数
    • construct():调用构造函数创建实例

设计目的

  • Object属于语言内部的方法放到Reflect
  • 将某些Object方法报错情况改成返回false
  • Object操作变成函数行为
  • ProxyReflect相辅相成

废弃方法

  • Object.defineProperty() => Reflect.defineProperty()
  • Object.getOwnPropertyDescriptor() => Reflect.getOwnPropertyDescriptor()

重点难点

  • Proxy方法Reflect方法一一对应
  • ProxyReflect联合使用,前者负责拦截赋值操作,后者负责完成赋值操作

数据绑定:观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const observerQueue = new Set();
const observe = fn => observerQueue.add(fn);
const observable = obj => new Proxy(obj, {
set(tgt, key, val, receiver) {
const result = Reflect.set(tgt, key, val, receiver);
observerQueue.forEach(v => v());
return result;
}
});

const person = observable({ age: 25, name: "Yajun" });
const print = () => console.log(`${person.name} is ${person.age} years old`);
observe(print);
person.name = "Joway";

Class

  • 定义:对一类具有共同特征的事物的抽象(构造函数语法糖)

  • 原理:类本身指向构造函数,所有方法定义在prototype上,可看作构造函数的另一种写法(Class === Class.prototype.constructor)

  • 方法和关键字

    • constructor():构造函数,new命令生成实例时自动调用
    • extends:继承父类
    • super:新建父类的this
    • static:定义静态属性方法
    • get:取值函数,拦截属性的取值行为
    • set:存值函数,拦截属性的存值行为
  • 属性

    • proto构造函数的继承(总是指向父类)
    • proto.proto:子类的原型的原型,即父类的原型(总是指向父类的__proto__)
    • prototype.proto属性方法的继承(总是指向父类的prototype)
  • 静态属性:定义类完成后赋值属性,该属性不会被实例继承,只能通过类来调用

  • 静态方法:使用static定义方法,该方法不会被实例继承,只能通过类来调用(方法中的this指向类,而不是实例)

  • 继承

    • 父类静态属性方法可被子类继承
    • 子类继承父类后,可从super上调用父类静态属性方法
    • 作为函数调用:只能在构造函数中调用super(),内部this指向继承的当前子类(super()调用后才可在构造函数中使用this)
    • 作为对象调用:在普通方法中指向父类的原型对象,在静态方法中指向父类
    • ES5实质:先创造子类实例的this,再将父类的属性方法添加到this上(Parent.apply(this))
    • ES6实质:先将父类实例的属性方法加到this上(调用super()),再用子类构造函数修改this
    • 实质
    • super
    • 显示定义:使用constructor() { super(); }定义继承父类,没有书写则显示定义
    • 子类继承父类:子类使用父类的属性方法时,必须在构造函数中调用super(),否则得不到父类的this
  • 实例:类相当于实例的原型,所有在类中定义的属性方法都会被实例继承

    • 显式指定属性方法:使用this指定到自身上(使用Class.hasOwnProperty()可检测到)
    • 隐式指定属性方法:直接声明定义在对象原型上(使用Class.__proto__.hasOwnProperty()可检测到)
  • 表达式

    • 类表达式:const Class = class {}
    • name属性:返回紧跟class后的类名
    • 属性表达式:[prop]
    • Generator方法:* mothod() {}
    • Async方法:async mothod() {}
  • this指向:解构实例属性或方法时会报错

    • 绑定this:this.mothod = this.mothod.bind(this)
    • 箭头函数:this.mothod = () => this.mothod()
  • 属性定义位置

    • 定义在构造函数中并使用this指向
    • 定义在类最顶层
  • new.target:确定构造函数是如何调用

原生构造函数

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • Date()
  • RegExp()
  • Error()

重点难点

  • 在实例上调用方法,实质是调用原型上的方法
  • Object.assign()可方便地一次向类添加多个方法(Object.assign(Class.prototype, { ... }))
  • 类内部所有定义的方法是不可枚举的(non-enumerable)
  • 构造函数默认返回实例对象(this),可指定返回另一个对象
  • 取值函数和存值函数设置在属性的Descriptor对象
  • 类不存在变量提升
  • 利用new.target === Class写出不能独立使用必须继承后才能使用的类
  • 子类继承父类后,this指向子类实例,通过super对某个属性赋值,赋值的属性会变成子类实例的属性
  • 使用super时,必须显式指定是作为函数还是作为对象使用
  • extends不仅可继承类还可继承原生的构造函数

私有属性方法

1
2
3
4
5
6
7
8
9
10
11
const name = Symbol("name");
const print = Symbol("print");
class Person {
constructor(age) {
this[name] = "Bruce";
this.age = age;
}
[print]() {
console.log(`${this[name]} is ${this.age} years old`);
}
}

继承混合类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function CopyProperties(target, source) {
for (const key of Reflect.ownKeys(source)) {
if (key !== "constructor" && key !== "prototype" && key !== "name") {
const desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
function MixClass(...mixins) {
class Mix {
constructor() {
for (const mixin of mixins) {
CopyProperties(this, new mixin());
}
}
}
for (const mixin of mixins) {
CopyProperties(Mix, mixin);
CopyProperties(Mix.prototype, mixin.prototype);
}
return Mix;
}
class Student extends MixClass(Person, Kid) {}

Module

  • 命令

    • 默认导入导出export { default } from "person"
    • 整体导入导出export * from "person"
    • 按需导入导出export { age, name, sex } from "person"
    • 改名导入导出export { name as newName } from "person"
    • 具名改默认导入导出export { name as default } from "person"
    • 默认改具名导入导出export { default as name } from "person"
    • 默认导入import Person from "person"
    • 整体导入import * as Person from "person"
    • 按需导入import { age, name, sex } from "person"
    • 改名导入import { name as newName } from "person"
    • 自执导入import "person"
    • 复合导入import Person, { name } from "person"
    • 默认导出export default Person(导入时可指定模块任意名称,无需知晓内部真实名称)
    • 单独导出export const name = "Bruce"
    • 按需导出export { age, name, sex }(推荐)
    • 改名导出export { name as newName }
    • export:规定模块对外接口
    • import:导入模块内部功能
    • 复合模式export命令import命令结合在一起写成一行,变量实质没有被导入当前模块,相当于对外转发接口,导致当前模块无法直接使用其导入变量
  • 继承:默认导出改名导出结合使用可使模块具备继承性

  • 设计思想:尽量地静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量

  • 严格模式:ES6模块自动采用严格模式(不管模块头部是否添加use strict)

模块方案

  • CommonJS:用于服务器(动态化依赖)
  • AMD:用于浏览器(动态化依赖)
  • CMD:用于浏览器(动态化依赖)
  • UMD:用于浏览器和服务器(动态化依赖)
  • ESM:用于浏览器和服务器(静态化依赖)

加载方式

  • 运行时加载

    • 定义:整体加载模块生成一个对象,再从对象上获取需要的属性和方法进行加载(全部加载)
    • 影响:只有运行时才能得到这个对象,导致无法在编译时做静态优化
  • 编译时加载

    • 定义:直接从模块中获取需要的属性和方法进行加载(按需加载)
    • 影响:在编译时就完成模块加载,效率比其他方案高,但无法引用模块本身(本身不是对象),可拓展JS高级语法(宏和类型校验)

加载实现

  • 传统加载:通过``进行同步或异步加载脚本

    • 同步加载:``
    • Defer异步加载:``(顺序加载,渲染完再执行)
    • Async异步加载:``(乱序加载,下载完就执行)
  • 模块加载:``(默认是Defer异步加载)

CommonJS和ESM的区别

  • CommonJS输出值的拷贝ESM输出值的引用

    • CommonJS一旦输出一个值,模块内部的变化就影响不到这个值
    • ESM是动态引用且不会缓存值,模块里的变量绑定其所在的模块,等到脚本真正执行时,再根据这个只读引用到被加载的那个模块里去取值
  • CommonJS是运行时加载,ESM是编译时加载

    • CommonJS加载模块是对象(即module.exports),该对象只有在脚本运行完才会生成
    • ESM加载模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

Node加载

  • 背景:CommonJSESM互不兼容,目前解决方案是将两者分开,采用各自的加载方案

  • 区分:要求ESM采用.mjs后缀文件名

    • require()不能加载.mjs文件,只有import命令才可加载.mjs文件
    • .mjs文件里不能使用require(),必须使用import命令加载文件
  • 驱动:node --experimental-modules file.mjs

  • 限制:Node的import命令目前只支持加载本地模块(file:协议),不支持加载远程模块

  • 加载优先级

    • 脚本文件省略后缀名:依次尝试加载四个后缀名文件(.mjs.js.jsonnode)
    • 以上不存在:尝试加载package.jsonmain字段指定的脚本
    • 以上不存在:依次尝试加载名称为index四个后缀名文件(.mjs.js.jsonnode)
    • 以上不存在:报错
  • 不存在的内部变量:argumentsexportsmodulerequirethis__dirname__filename

  • CommonJS加载ESM

    • 不能使用require(),只能使用import()
  • ESM加载CommonJS

    • 自动将module.exports转化成export default
    • CommonJS输出缓存机制在ESM加载方式下依然有效
    • 采用import命令加载CommonJS模块时,不允许采用按需导入,应使用默认导入整体导入

循环加载

  • 定义:脚本A的执行依赖脚本B,而脚本A的执行又依赖脚本B

  • 加载原理

    • CommonJS:require()首次加载脚本就会执行整个脚本,在内存里生成一个对象缓存下来,二次加载脚本时直接从缓存中获取
    • ESM:import命令加载变量不会被缓存,而是成为一个指向被加载模块的引用
  • 循环加载

    • CommonJS:只输出已经执行的部分,还未执行的部分不会输出
    • ESM:需开发者自己保证真正取值时能够取到值(可把变量写成函数形式,函数具有提升作用)

重点难点

  • ES6模块中,顶层this指向undefined,不应该在顶层代码使用this
  • 一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取
  • export命令输出的接口与其对应的值是动态绑定关系,即通过该接口可获取模块内部实时的值
  • import命令大括号里的变量名必须与被导入模块对外接口的名称相同
  • import命令输入的变量只读(本质是输入接口),不允许在加载模块的脚本里改写接口
  • import命令命令具有提升效果,会提升到整个模块的头部,首先执行
  • 重复执行同一句import语句,只会执行一次
  • export default命令只能使用一次
  • export default命令导出的整体模块,在执行import命令时其后不能跟大括号
  • export default命令本质是输出一个名为default的变量,后面不能跟变量声明语句
  • export default命令本质是将后面的值赋给名为default的变量,可直接将值写在其后
  • export default命令export {}命令可同时存在,对应复合导入
  • export命令import命令可出现在模块任何位置,只要处于模块顶层即可,不能处于块级作用域
  • import()加载模块成功后,此模块会作为一个对象,当作then()的参数,可使用对象解构赋值来获取输出接口
  • 同时动态加载多个模块时,可使用Promise.all()import()相结合来实现
  • import()和结合async/await来书写同步操作的代码

单例模式:跨模块常量

1
2
3
4
5
6
7
8
9
10
11
12
// 常量跨文件共享
// person.js
const NAME = "Bruce";
const AGE = 25;
const SEX = "male";
export { AGE, NAME, SEX };
// file1.js
import { AGE } from "person";
console.log(AGE);
// file2.js
import { AGE, NAME, SEX } from "person";
console.log(AGE, NAME, SEX);

默认导入互换整体导入

1
2
3
4
import Person from "person";
console.log(Person.AGE);
import * as Person from "person";
console.log(Person.default.AGE);

Iterator

  • 定义:为各种不同的数据结构提供统一的访问机制

  • 原理:创建一个指针指向首个成员,按照次序使用next()指向下一个成员,直接到结束位置(数据结构只要部署Iterator接口就可完成遍历操作)

  • 作用

    • 为各种数据结构提供一个统一的简便的访问接口
    • 使得数据结构成员能够按某种次序排列
    • ES6创造了新的遍历命令for-ofIterator接口主要供for-of消费
  • 形式:for-of(自动去寻找Iterator接口)

  • 数据结构

    • 集合:ArrayObjectSetMap
    • 原生具备接口的数据结构:StringArraySetMapTypedArrayArgumentsNodeList
  • 部署:默认部署在Symbol.iterator(具备此属性被认为可遍历的iterable)

  • 遍历器对象

    • next():下一步操作,返回{ done, value }(必须部署)
    • return()for-of提前退出调用,返回{ done: true }
    • throw():不使用,配合Generator函数使用

ForOf循环

  • 定义:调用Iterator接口产生遍历器对象(for-of内部调用数据结构的Symbol.iterator())

  • 遍历字符串:for-in获取索引for-of获取(可识别32位UTF-16字符)

  • 遍历数组:for-in获取索引for-of获取

  • 遍历对象:for-in获取for-of需自行部署

  • 遍历Set:for-of获取 => for (const v of set)

  • 遍历Map:for-of获取键值对 => for (const [k, v] of map)

  • 遍历类数组:包含length的对象Arguments对象NodeList对象(无Iterator接口的类数组可用Array.from()转换)

  • 计算生成数据结构:ArraySetMap

    • keys():返回遍历器对象,遍历所有的键
    • values():返回遍历器对象,遍历所有的值
    • entries():返回遍历器对象,遍历所有的键值对
  • for-in区别

    • 有着同for-in一样的简洁语法,但没有for-in那些缺点、
    • 不同于forEach(),它可与breakcontinuereturn配合使用
    • 提供遍历所有数据结构的统一操作接口

应用场景

  • 改写具有Iterator接口的数据结构的Symbol.iterator
  • 解构赋值:对Set进行结构
  • 扩展运算符:将部署Iterator接口的数据结构转为数组
  • yield:`yield`后跟一个可遍历的数据结构,会调用其遍历器接口
  • 接受数组作为参数的函数:for-ofArray.from()new Set()new WeakSet()new Map()new WeakMap()Promise.all()Promise.race()

Promise

  • 定义:包含异步操作结果的对象

  • 状态

    • 进行中pending
    • 已成功resolved
    • 已失败rejected
  • 特点

    • 对象的状态不受外界影响
    • 一旦状态改变就不会再变,任何时候都可得到这个结果
  • 声明:new Promise((resolve, reject) => {})

  • 出参

    • resolve:将状态从未完成变为成功,在异步操作成功时调用,并将异步操作的结果作为参数传递出去
    • reject:将状态从未完成变为失败,在异步操作失败时调用,并将异步操作的错误作为参数传递出去
  • 方法

    • Promise实例:原封不动地返回入参
    • Thenable对象:将此对象转为Promise对象并返回(Thenable为包含then()的对象,执行then()相当于执行此对象的then())
    • 不具有then()的对象:将此对象转为Promise对象并返回,状态为resolved
    • 不带参数:返回Promise对象,状态为resolved
    • 入参:具有Iterator接口的数据结构
    • 成功:只有全部实例状态变成resolved,最终状态才会变成resolved
    • 失败:其中一个实例状态变成rejected,最终状态就会变成rejected
    • 第一参数:状态变为resolved时调用
    • 第二参数:状态变为rejected时调用(可选)
    • then():分别指定resolved状态rejected状态的回调函数
    • catch():指定发生错误时的回调函数
    • Promise.all():将多个实例包装成一个新实例,返回全部实例状态变更后的结果数组(齐变更再返回)
    • Promise.race():将多个实例包装成一个新实例,返回全部实例状态优先变更后的结果(先变更先返回)
    • Promise.resolve():将对象转为Promise对象(等价于new Promise(resolve => resolve()))
    • Promise.reject():将对象转为状态为rejected的Promise对象(等价于new Promise((resolve, reject) => reject()))

应用场景

  • 加载图片
  • AJAX转Promise对象

重点难点

  • 只有异步操作的结果可决定当前状态是哪一种,其他操作都无法改变这个状态
  • 状态改变只有两种可能:从pending变为resolved、从pending变为rejected
  • 一旦新建Promise对象就会立即执行,无法中途取消
  • 不设置回调函数,内部抛错不会反应到外部
  • 当处于pending时,无法得知目前进展到哪一个阶段
  • 实例状态变为resolvedrejected时,会触发then()绑定的回调函数
  • resolve()reject()的执行总是晚于本轮循环的同步任务
  • then()返回新实例,其后可再调用另一个then()
  • then()运行中抛出错误会被catch()捕获
  • reject()的作用等同于抛出错误
  • 实例状态已变成resolved时,再抛出错误是无效的,不会被捕获,等于没有抛出
  • 实例状态的错误具有冒泡性质,会一直向后传递直到被捕获为止,错误总是会被下一个catch()捕获
  • 不要在then()里定义rejected状态的回调函数(不使用其第二参数)
  • 建议使用catch()捕获错误,不要使用then()第二个参数捕获
  • 没有使用catch()捕获错误,实例抛错不会传递到外层代码,即不会有任何反应
  • 作为参数的实例定义了catch(),一旦被rejected并不会触发Promise.all()catch()
  • Promise.reject()的参数会原封不动地作为rejected的理由,变成后续方法的参数

Generator

  • 定义:封装多个内部状态的异步编程解决方案

  • 形式:调用Generator函数(该函数不执行)返回指向内部状态的指针对象(不是运行结果)

  • 声明:function* Func() {}

  • 方法

    • next():使指针移向下一个状态,返回{ done, value }(入参会被当作上一个yield命令表达式的返回值)
    • return():返回指定值且终结遍历Generator函数,返回{ done: true, value: 入参 }
    • throw():在Generator函数体外抛出错误,在Generator函数体内捕获错误,返回自定义的new Errow()
  • yield命令:声明内部状态的值(return声明结束返回的值)

    • 遇到yield命令就暂停执行后面的操作,并将其后表达式的值作为返回对象的value
    • 下次调用next()时,再继续往下执行直到遇到下一个yield命令
    • 没有再遇到yield命令就一直运行到Generator函数结束,直到遇到return语句为止并将其后表达式的值作为返回对象的value
    • Generator函数没有return语句则返回对象的valueundefined
  • yield*命令:在一个Generator函数里执行另一个Generator函数(后随具有Iterator接口的数据结构)

  • 遍历:通过for-of自动调用next()

  • 作为对象属性

    • 全写:const obj = { method: function*() {} }
    • 简写:const obj = { * method() {} }
  • 上下文:执行产生的上下文环境一旦遇到yield命令就会暂时退出堆栈(但并不消失),所有变量和对象会冻结在当前状态,等到对它执行next()时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行

方法异同

  • 相同点:next()throw()return()本质上是同一件事,作用都是让函数恢复执行且使用不同的语句替换yield命令

  • 不同点

    • next():将yield命令替换成一个
    • return():将yield命令替换成一个return语句
    • throw():将yield命令替换成一个throw语句

应用场景

  • 异步操作同步化表达
  • 控制流管理
  • 为对象部署Iterator接口:把Generator函数赋值给对象的Symbol.iterator,从而使该对象具有Iterator接口
  • 作为具有Iterator接口的数据结构

重点难点

  • 每次调用next(),指针就从函数头部上次停下的位置开始执行,直到遇到下一个yield命令return语句为止
  • 函数内部可不用yield命令,但会变成单纯的暂缓执行函数(还是需要next()触发)
  • yield命令是暂停执行的标记,next()是恢复执行的操作
  • yield命令用在另一个表达式中必须放在圆括号
  • yield命令用作函数参数或放在赋值表达式的右边,可不加圆括号
  • yield命令本身没有返回值,可认为是返回undefined
  • yield命令表达式为惰性求值,等next()执行到此才求值
  • 函数调用后生成遍历器对象,此对象的Symbol.iterator是此对象本身
  • 在函数运行的不同阶段,通过next()从外部向内部注入不同的值,从而调整函数行为
  • 首个next()用来启动遍历器对象,后续才可传递参数
  • 想首次调用next()时就能输入值,可在函数外面再包一层
  • 一旦next()返回对象的donetruefor-of遍历会中止且不包含该返回对象
  • 函数内部部署try-finally且正在执行try,那么return()会导致立刻进入finally,执行完finally以后整个函数才会结束
  • 函数内部没有部署try-catchthrow()抛错将被外部try-catch捕获
  • throw()抛错要被内部捕获,前提是必须至少执行过一次next()
  • throw()被捕获以后,会附带执行下一条yield命令
  • 函数还未开始执行,这时throw()抛错只可能抛出在函数外部

首次next()可传值

1
2
3
4
5
6
7
8
9
10
11
12
function Wrapper(func) {
return function(...args) {
const generator = func(...args);
generator.next();
return generator;
}
}
const print = Wrapper(function*() {
console.log(`First Input: ${yield}`);
return "done";
});
print().next("hello");

ES2016

数值扩展

  • 指数运算符(\)**:数值求幂(相当于Math.pow())

数组扩展

  • includes():是否存在指定成员

ES2017

声明

  • 共享内存和原子操作:由全局对象SharedArrayBufferAtomics实现,将数据存储在一块共享内存空间中,这些数据可在JS主线程web-worker线程之间共享

字符串扩展

  • padStart():把指定字符串填充到字符串头部,返回新字符串
  • padEnd():把指定字符串填充到字符串尾部,返回新字符串

对象扩展

  • Object.getOwnPropertyDescriptors():返回对象所有自身属性(非继承属性)的描述对象
  • Object.values():返回以值组成的数组
  • Object.entries():返回以键和值组成的数组

函数扩展

  • 函数参数尾逗号:允许函数最后一个参数有尾逗号

Async

  • 定义:使异步函数以同步函数的形式书写(Generator函数语法糖)

  • 原理:将Generator函数和自动执行器spawn包装在一个函数里

  • 形式:将Generator函数*替换成async,将yield替换成await

  • 声明

    • 具名函数:async function Func() {}
    • 函数表达式:const func = async function() {}
    • 箭头函数:const func = async() => {}
    • 对象方法:const obj = { async func() {} }
    • 类方法:class Cla { async Func() {} }
  • await命令:等待当前Promise对象状态变更完毕

    • 正常情况:后面是Promise对象则返回其结果,否则返回对应的值
    • 后随Thenable对象:将其等同于Promise对象返回其结果
  • 错误处理:将await命令Promise对象放到try-catch中(可放多个)

Async对Generator改进

  • 内置执行器
  • 更好的语义
  • 更广的适用性
  • 返回值是Promise对象

应用场景

  • 按顺序完成异步操作

重点难点

  • Async函数返回Promise对象,可使用then()添加回调函数

  • 内部return返回值会成为后续then()的出参

  • 内部抛出错误会导致返回的Promise对象变为rejected状态,被catch()接收到

  • 返回的Promise对象必须等到内部所有await命令Promise对象执行完才会发生状态改变,除非遇到return语句抛出错误

  • 任何一个await命令Promise对象变为rejected状态,整个Async函数都会中断执行

  • 希望即使前一个异步操作失败也不要中断后面的异步操作

    • await命令Promise对象放到try-catch
    • await命令Promise对象跟一个catch()
  • await命令Promise对象可能变为rejected状态,最好把其放到try-catch

  • 多个await命令Promise对象若不存在继发关系,最好让它们同时触发

  • await命令只能用在Async函数之中,否则会报错

  • 数组使用forEach()执行async/await会失效,可使用for-ofPromise.all()代替

  • 可保留运行堆栈,函数上下文随着Async函数的执行而存在,执行完成就消失

ES2018

字符串扩展

  • 放松对标签模板里字符串转义的限制:遇到不合法的字符串转义返回undefined,并且从raw上可获取原字符串

对象扩展

  • 扩展运算符(…):转换对象为用逗号分隔的参数序列({ ...obj },相当于rest/spread参数的逆运算)

扩展应用

  • 克隆对象:const obj = { __proto__: Object.getPrototypeOf(obj1), ...obj1 }
  • 合并对象:const obj = { ...obj1, ...obj2 }
  • 转换字符串为对象:{ ..."hello" }
  • 转换数组为对象:{ ...[1, 2] }
  • 与对象解构赋值结合:const { x, ...rest/spread } = { x: 1, y: 2, z: 3 }(不能复制继承自原型对象的属性)
  • 修改现有对象部分属性:const obj = { x: 1, ...{ x: 2 } }

正则扩展

  • s修饰符:dotAll模式修饰符,使.匹配任意单个字符(dotAll模式)

  • dotAll:是否设置s修饰符

  • 后行断言x只有在y后才匹配

  • 后行否定断言x只有不在y后才匹配

  • Unicode属性转义:匹配符合Unicode某种属性的所有字符

    • 正向匹配:\p{PropRule}
    • 反向匹配:\P{PropRule}
    • 限制:\p{...}\P{...}只对Unicode字符有效,使用时需加上u修饰符
  • 具名组匹配:为每组匹配指定名字(?)

    • 声明:const time = "2017-09-11"const regexp = /(?\d{4})-(?\d{2})-(?\d{2})/u
    • 匹配:time.replace(regexp, "$/$/$")
    • 形式:str.exec().groups.GroupName
    • 解构赋值替换

Promise

  • finally():指定不管最后状态如何都会执行的回调函数

Async

  • 异步迭代器(for-wait-of):,循环等待每个Promise对象变为resolved状态才进入下一步

ES2019

字符串扩展

  • 直接输入U+2028和U+2029:字符串可直接输入行分隔符段分隔符
  • JSON.stringify()改造:可返回不符合UTF-8标准的字符串
  • trimStart():消除字符串头部空格,返回新字符串
  • trimEnd():消除字符串尾部空格,返回新字符串

对象扩展

  • Object.fromEntries():返回以键和值组成的对象(Object.entries()的逆操作)

数组扩展

  • flat():扁平化数组,返回新数组
  • flatMap():映射且扁平化数组,返回新数组(只能展开一层数组)

函数扩展

  • toString()改造:返回函数原始代码(与编码一致)
  • catch()参数可省略catch()中的参数可省略

Symbol

  • description:返回Symbol值的描述

ES提案

声明

  • globalThis对象:作为顶层对象,指向全局环境下的this
  • do表达式:封装块级作用域的操作,返回内部最后执行表达式的值(do{})
  • throw表达式:直接使用throw new Error(),无需(){}包括
  • !#命令:指定脚本执行器(写在文件首行)

数值扩展

  • 数值分隔符(_):使用_作为千分位分隔符(增加数值的可读性)
  • BigInt():创建任何位数的整数(新增的数据类型,使用n结尾)

对象扩展

  • 链判断操作符(?.):是否存在对象属性(不存在返回undefined且不再往下执行)
  • 空判断操作符(??):是否值为undefinednull,是则使用默认值

函数扩展

  • 函数部分执行:复用函数功能(?表示单个参数占位符,...表示多个参数占位符)

  • 管道操作符(|>):把左边表达式的值传入右边的函数进行求值(f(x) => x |> f)

  • 绑定运算符(::):函数绑定(左边是对象右边是函数,取代bindapplycall调用)

    • bind:bar.bind(foo) => foo::bar
    • apply:bar.apply(foo, arguments) => foo::bar(...arguments)

Proxy

  • Promise.try():不想区分是否同步异步函数,包装函数为实例,使用then()指定下一步流程,使用catch()捕获错误

Realm

  • 定义:提供沙箱功能,允许隔离代码,防止被隔离的代码拿到全局对象
  • 声明:new Realm().global

Class

  • 静态属性:使用static定义属性,该属性不会被实例继承,只能通过类来调用
  • 私有属性:使用#定义属性,该属性只能在类内部访问
  • 私有方法:使用#定义方法,该方法只能在类内部访问
  • 装饰器:使用@注释或修改类和类方法

Module

  • import():动态导入(返回Primise)

    • 背景:import命令被JS引擎静态分析,先于模块内的其他语句执行,无法取代require()的动态加载功能,提案建议引入import()来代替require()
    • 位置:可在任何地方使用
    • 区别:require()同步加载import()异步加载
    • 场景:按需加载、条件加载、模块路径动态化
  • import.meta:返回脚本元信息

Async

  • 顶层Await:允许在模块的顶层独立使用await命令(借用await解决模块异步加载的问题)

常用的JS开发技巧(I)

目录

  • 「String Skill」:字符串技巧
  • 「Number Skill」:数值技巧
  • 「Boolean Skill」:布尔技巧
  • 「Array Skill」:数组技巧
  • 「Object Skill」:对象技巧
  • 「Function Skill」:函数技巧
  • 「DOM Skill」:DOM技巧

备注

  • 代码只作演示用途,不会详细说明ES6语法
  • 如有不明白的语法问题可以参考阮一峰老师的《ES6标准入门》
  • 《ES6标准入门》一直保持更新,建议收藏,平时查看

String Skill

对比时间

时间个位数形式需补0

1
2
3
4
const time1 = "2019-02-14 21:00:00";
const time2 = "2019-05-01 09:00:00";
const overtime = time1 > time2;
// overtime => false
格式化金钱
1
2
3
const ThousandNum = num => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const money = ThousandNum(20190214);
// money => "20,190,214"
生成随机ID
1
2
3
const RandomId = len => Math.random().toString(36).substr(3, len);
const id = RandomId(10);
// id => "jg7zpgiqva"
生成随机HEX色值
1
2
3
const RandomColor = () => "#" + Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, "0");
const color = RandomColor();
// color => "#f03665"
生成星级评分
1
2
3
const StartScore = rate => "★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);
const start = StartScore(3);
// start => "★★★"
操作URL查询参数
1
2
3
const params = new URLSearchParams(location.search.replace(/\?/ig, "")); // location.search = "?name=young&sex=male"
params.has("young"); // true
params.get("sex"); // "male"

Number Skill

取整

代替正数的Math.floor(),代替负数的Math.ceil()

1
2
3
4
const num1 = ~~ 1.69;
const num2 = 1.69 | 0;
const num3 = 1.69 >> 0;
// num1 num2 num3 => 1 1 1
补零
1
2
3
const FillZero = (num, len) => num.toString().padStart(len, "0");
const num = FillZero(169, 5);
// num => "00169"
转数值

只对null、""、false、数值字符串有效

1
2
3
4
5
const num1 = +null;
const num2 = +"";
const num3 = +false;
const num4 = +"169";
// num1 num2 num3 num4 => 0 0 0 169
时间戳
1
2
const timestamp = +new Date("2019-02-14");
// timestamp => 1550102400000
精确小数
1
2
3
const RoundNum = (num, decimal) => Math.round(num * 10 ** decimal) / 10 ** decimal;
const num = RoundNum(1.69, 1);
// num => 1.7
判断奇偶
1
2
3
const OddEven = num => !!(num & 1) ? "odd" : "even";
const num = OddEven(2);
// num => "even"
取最小最大值
1
2
3
4
const arr = [0, 1, 2];
const min = Math.min(...arr);
const max = Math.max(...arr);
// min max => 0 2
生成范围随机数
1
2
const RandomNum = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const num = RandomNum(1, 10);

Boolean Skill

短路运算符
1
2
3
const a = d && 1; // 满足条件赋值:取假运算,从左到右依次判断,遇到假值返回假值,后面不再执行,否则返回最后一个真值
const b = d || 1; // 默认赋值:取真运算,从左到右依次判断,遇到真值返回真值,后面不再执行,否则返回最后一个假值
const c = !d; // 取假赋值:单个表达式转换为true则返回false,否则返回true
判断数据类型

可判断类型:undefined、null、string、number、boolean、array、object、symbol、date、regexp、function、asyncfunction、arguments、set、map、weakset、weakmap

1
2
3
4
5
6
7
8
9
function DataType(tgt, type) {
const dataType = Object.prototype.toString.call(tgt).replace(/\[object (\w+)\]/, "$1").toLowerCase();
return type ? dataType === type : dataType;
}
DataType("young"); // "string"
DataType(20190214); // "number"
DataType(true); // "boolean"
DataType([], "array"); // true
DataType({}, "array"); // false
是否为空数组
1
2
3
const arr = [];
const flag = Array.isArray(arr) && !arr.length;
// flag => true
是否为空对象
1
2
3
const obj = {};
const flag = DataType(obj, "object") && !Object.keys(obj).length;
// flag => true
满足条件时执行
1
2
3
4
5
6
const flagA = true; // 条件A
const flagB = false; // 条件B
(flagA || flagB) && Func(); // 满足A或B时执行
(flagA || !flagB) && Func(); // 满足A或不满足B时执行
flagA && flagB && Func(); // 同时满足A和B时执行
flagA && !flagB && Func(); // 满足A且不满足B时执行
为非假值时执行
1
2
const flag = false; // undefined、null、""、0、false、NaN
!flag && Func();
数组不为空时执行
1
2
const arr = [0, 1, 2];
arr.length && Func();
对象不为空时执行
1
2
const obj = { a: 0, b: 1, c: 2 };
Object.keys(obj).length && Func();
函数退出代替条件分支退出
1
2
3
4
5
6
7
8
if (flag) {
Func();
return false;
}
// 换成
if (flag) {
return Func();
}
switch/case使用区间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const age = 26;
switch (true) {
case isNaN(age):
console.log("not a number");
break;
case (age < 18):
console.log("under age");
break;
case (age >= 18):
console.log("adult");
break;
default:
console.log("please set your age");
break;
}

Array Skill

克隆数组
1
2
3
const _arr = [0, 1, 2];
const arr = [..._arr];
// arr => [0, 1, 2]
合并数组
1
2
3
4
const arr1 = [0, 1, 2];
const arr2 = [3, 4, 5];
const arr = [...arr1, ...arr2];
// arr => [0, 1, 2, 3, 4, 5];
去重数组
1
2
const arr = [...new Set([0, 1, 1, null, null])];
// arr => [0, 1, null]
混淆数组
1
2
const arr = [0, 1, 2, 3, 4, 5].slice().sort(() => Math.random() - .5);
// arr => [3, 4, 0, 5, 1, 2]
清空数组
1
2
3
const arr = [0, 1, 2];
arr.length = 0;
// arr => []
截断数组
1
2
3
const arr = [0, 1, 2];
arr.length = 2;
// arr => [0, 1]
交换赋值
1
2
3
4
let a = 0;
let b = 1;
[a, b] = [b, a];
// a b => 1 0
过滤空值

空值:undefined、null、””、0、false、NaN

1
2
const arr = [undefined, null, "", 0, false, NaN, 1, 2].filter(Boolean);
// arr => [1, 2]
异步累计
1
2
3
4
5
6
7
8
9
async function Func(deps) {
return deps.reduce(async(t, v) => {
const dep = await t;
const version = await Todo(v);
dep[v] = version;
return dep;
}, Promise.resolve({}));
}
const result = await Func(); // 需在async包围下使用
数组首部插入成员
1
2
3
4
5
let arr = [1, 2]; // 以下方法任选一种
arr.unshift(0);
arr = [0].concat(arr);
arr = [0, ...arr];
// arr => [0, 1, 2]
数组尾部插入成员
1
2
3
4
5
6
let arr = [0, 1]; // 以下方法任选一种
arr.push(2);
arr.concat(2);
arr[arr.length] = 2;
arr = [...arr, 2];
// arr => [0, 1, 2]
统计数组成员个数
1
2
3
4
5
6
const arr = [0, 1, 1, 2, 2, 2];
const count = arr.reduce((t, v) => {
t[v] = t[v] ? ++t[v] : 1;
return t;
}, {});
// count => { 0: 1, 1: 2, 2: 3 }
解构数组成员嵌套
1
2
3
const arr = [0, 1, [2, 3, [4, 5]]];
const [a, b, [c, d, [e, f]]] = arr;
// a b c d e f => 0 1 2 3 4 5
解构数组成员别名
1
2
3
const arr = [0, 1, 2];
const { 0: a, 1: b, 2: c } = arr;
// a b c => 0 1 2
解构数组成员默认值
1
2
3
const arr = [0, 1, 2];
const [a, b, c = 3, d = 4] = arr;
// a b c d => 0 1 2 4
获取随机数组成员
1
2
3
const arr = [0, 1, 2, 3, 4, 5];
const randomItem = arr[Math.floor(Math.random() * arr.length)];
// randomItem => 1
创建指定长度数组
1
2
const arr = [...new Array(3).keys()];
// arr => [0, 1, 2]
创建指定长度且值相等的数组
1
2
const arr = new Array(3).fill(0);
// arr => [0, 0, 0]
reduce代替map和filter
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
const _arr = [0, 1, 2];

// map
const arr = _arr.map(v => v * 2);
const arr = _arr.reduce((t, v) => {
t.push(v * 2);
return t;
}, []);
// arr => [0, 2, 4]

// filter
const arr = _arr.filter(v => v > 0);
const arr = _arr.reduce((t, v) => {
v > 0 && t.push(v);
return t;
}, []);
// arr => [1, 2]

// map和filter
const arr = _arr.map(v => v * 2).filter(v => v > 2);
const arr = _arr.reduce((t, v) => {
v = v * 2;
v > 2 && t.push(v);
return t;
}, []);
// arr => [4]

Object Skill

克隆对象
1
2
3
4
const _obj = { a: 0, b: 1, c: 2 }; // 以下方法任选一种
const obj = { ..._obj };
const obj = JSON.parse(JSON.stringify(_obj));
// obj => { a: 0, b: 1, c: 2 }
合并对象
1
2
3
4
const obj1 = { a: 0, b: 1, c: 2 };
const obj2 = { c: 3, d: 4, e: 5 };
const obj = { ...obj1, ...obj2 };
// obj => { a: 0, b: 1, c: 3, d: 4, e: 5 }
对象字面量

获取环境变量时必用此方法,用它一直爽,一直用它一直爽

1
2
3
4
5
6
7
const env = "prod";
const link = {
dev: "Development Address",
test: "Testing Address",
prod: "Production Address"
}[env];
// link => "Production Address"
对象变量属性
1
2
3
4
5
6
7
const flag = false;
const obj = {
a: 0,
b: 1,
[flag ? "c" : "d"]: 2
};
// obj => { a: 0, b: 1, d: 2 }
创建纯空对象
1
2
3
const obj = Object.create(null);
Object.prototype.a = 0;
// obj => {}
删除对象无用属性
1
2
3
const obj = { a: 0, b: 1, c: 2 }; // 只想拿b和c
const { a, ...rest } = obj;
// rest => { b: 1, c: 2 }
解构对象属性嵌套
1
2
3
const obj = { a: 0, b: 1, c: { d: 2, e: 3 } };
const { c: { d, e } } = obj;
// d e => 2 3
解构对象属性别名
1
2
3
const obj = { a: 0, b: 1, c: 2 };
const { a, b: d, c: e } = obj;
// a d e => 0 1 2
解构对象属性默认值
1
2
3
const obj = { a: 0, b: 1, c: 2 };
const { a, b = 2, d = 3 } = obj;
// a b d => 0 1 3

Function Skill

函数自执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Func = function() {}(); // 常用

(function() {})(); // 常用
(function() {}()); // 常用
[function() {}()];

+ function() {}();
- function() {}();
~ function() {}();
! function() {}();

new function() {};
new function() {}();
void function() {}();
typeof function() {}();
delete function() {}();

1, function() {}();
1 ^ function() {}();
1 > function() {}();
隐式返回值

只能用于单语句返回值箭头函数,如果返回值是对象必须使用()包住

1
2
3
4
5
const Func = function(name) {
return "I Love " + name;
};
// 换成
const Func = name => "I Love " + name;
一次性函数

适用于运行一些只需执行一次的初始化代码

1
2
3
4
5
6
function Func() {
console.log("x");
Func = function() {
console.log("y");
}
}
惰性载入函数

函数内判断分支较多较复杂时可大大节约资源开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Func() {
if (a === b) {
console.log("x");
} else {
console.log("y");
}
}
// 换成
function Func() {
if (a === b) {
Func = function() {
console.log("x");
}
} else {
Func = function() {
console.log("y");
}
}
return Func();
}
检测非空参数
1
2
3
4
5
6
7
8
function IsRequired() {
throw new Error("param is required");
}
function Func(name = IsRequired()) {
console.log("I Love " + name);
}
Func(); // "param is required"
Func("You"); // "I Love You"
字符串创建函数
1
const Func = new Function("name", "console.log(\"I Love \" + name)");
优雅处理错误信息
1
2
3
4
5
try {
Func();
} catch (e) {
location.href = "https://stackoverflow.com/search?q=[js]+" + e.message;
}
优雅处理Async/Await参数
1
2
3
4
function AsyncTo(promise) {
return promise.then(data => [null, data]).catch(err => [err]);
}
const [err, res] = await AsyncTo(Func());
优雅处理多个函数返回值
1
2
3
4
5
6
7
function Func() {
return Promise.all([
fetch("/user"),
fetch("/comment")
]);
}
const [user, comment] = await Func(); // 需在async包围下使用

DOM Skill

显示全部DOM边框

调试页面元素边界时使用

1
2
3
[].forEach.call($$("*"), dom => {
dom.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
});
自适应页面

页面基于一张设计图但需做多款机型自适应,元素尺寸使用rem进行设置

1
2
3
4
5
6
function AutoResponse(width = 750) {
const target = document.documentElement;
target.clientWidth >= 600
? (target.style.fontSize = "80px")
: (target.style.fontSize = target.clientWidth / width * 100 + "px");
}
过滤XSS
1
2
3
4
5
6
7
function FilterXss(content) {
let elem = document.createElement("div");
elem.innerText = content;
const result = elem.innerHTML;
elem = null;
return result;
}
存取LocalStorage

反序列化取,序列化存

1
2
const love = JSON.parse(localStorage.getItem("love"));
localStorage.setItem("love", JSON.stringify("I Love You"));

总结

最后送大家一个键盘!

1
2
(_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["Bs","Tab","Caps","Enter"][p++]||'Shift',p])}\\|`,m+=y+(x+'    ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`)()

类型定义文件(*.d.ts)

类型定义文件

在 TypeScript 中,我们可以很简单的,在代码编写中定义类型:

1
2
3
4
5
6
7
8
9
10
interface IBaseModel {
say(keys: string[] | null): object
}

class User implements IBaseModel {
name: string
constructor (name: string) {
this.name = name
}
}

但是主流的库都是 JavaScript 编写的,TypeScript 身为 JavaScript 的超集,自然需要考虑到如何让 JS 库也能定义静态类型。

TypeScript 经过了一系列的摸索,先后提出了 tsd(已废弃)、typings(已废弃),最终在 TypeScript 2.0 的时候重新整理了类型定义,提出了 DefinitelyTyped

DefinitelyTyped 就是让你把 “类型定义文件(*.d.ts)”,发布到 npm 中,配合编辑器(或插件),就能够检测到 JS 库中的静态类型。

类型定义文件的以 .d.ts 结尾,里面主要用来定义类型。

例如这是 jQuery 的类型定义文件 中一段代码(为了方便理解做了一些改动)

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
// 定义 jQuery 需要用到的类型命名空间
declare namespace JQuery {
// 定义基本使用的类型
type Selector = string;
type TypeOrArray<T> = T | T[];
type htmlString = string;
}

// 定义 jQuery 接口,jquery 是一个 包含 Element 的集合
interface JQuery<TElement extends Node = HTMLElement> extends Iterable<TElement> {
length: number;
eq(index: number): this;

// 重载
add(selector: JQuery.Selector, context: Element): this;
add(selector: JQuery.Selector | JQuery.TypeOrArray<Element> | JQuery.htmlString | JQuery): this;

children(selector?: JQuery.Selector): this;
css(propertyName: string): string;
html(): string;
}

// 对模块 jquery 输出接口
declare module 'jquery' {
// module 中要使用 export = 而不是 export default
export = jQuery;
}

类型定义

*.d.ts 编写起来非常简单,经过 TypeScript 良好的静态类型系统洗礼过后,语法学习成本非常低。

我们可以使用 type 用来定义类型变量:

1
2
3
4
5
6
// 基本类型
type UserName = string

// 类型赋值
type WebSite = string
type Tsaid = WebSite

可以看到 type 其实可以定义各种格式的类型,也可以和其他类型进行组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对象
type User = {
name: string;
age: number;
website: WebSite;
}

// 方法
type say = (age: number) => string

// 类
class TaSaid {
website: string;
say: (age: number) => string;
}

当然,我们也可以使用 interface 定义我们的复杂类型,在 TS 中我们也可以直接定义 interface

1
2
3
4
interface Application {
init(): void
get(key: string): object
}

interfacetype(或者说 class) 很像。

但是 type 的含义是定义自定义类型,当 TS 提供给你的基础类型都不满足的时候,可以使用 type 自由组合出你的新类型,而 interface 应该是对外输出的接口。

type 不可以被继承,但 interface 可以:

1
2
3
4
5
6
7
8
interface BaseApplication {
appId: number
}

export interface Application extends BaseApplication {
init(): void
get(key: string): object
}

declare

declare 可以创建 *.d.ts 文件中的变量,declare 只能作用域最外层:

1
2
3
4
5
6
7
8
9
declare var foo: number;
declare function greet(greeting: string): void;

declare namespace tasaid {
// 这里不能 declare
interface blog {
website: 'http://tasaid.com'
}
}

基本上顶层的定义都需要使用 declareclass 也是:

1
2
3
declare class User {
name: string
}

namespace

为防止类型重复,使用 namespace 用于划分区域块,分离重复的类型,顶层的 namespace 需要 declare 输出到外部环境,子命名空间不需要 declare

1
2
3
4
5
6
7
8
9
10
11
// 命名空间
declare namespace Models {
type A = number
// 子命名空间
namespace Config {
type A = object
type B = string
}
}

type C = Models.Config.A

组合定义

上面我们只演示了一些简单的类型组合,生产环境中会包含许多复杂的类型定义,这时候我们就需要各种组合出强大的类型定义:

动态属性

有些类型的属性名是动态而未知的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
'10086': {
name: '中国移动',
website: 'http://www.10086.cn',
},
'10010': {
name: '中国联通',
website: 'http://www.10010.com',
},
'10000': {
name: '中国电信',
website: 'http://www.189.cn'
}
}

我们可以使用动态属性名来定义类型:

1
2
3
4
5
6
7
8
9
interface ChinaMobile {
name: string;
website: string;
}

interface ChinaMobileList {
// 动态属性
[phone: string]: ChinaMobile
}

类型遍历

当你已知某个类型范围的时候,可以使用 inkeyof 来遍历类型,例如上面的 ChinaMobile 例子,我们可以使用 in 来约束属性名必须为三家运营商之一:

1
2
3
4
5
6
7
8
9
10
11
12
type ChinaMobilePhones = '10086' | '10010' | '10000'

interface ChinaMobile {
name: string;
website: string;
}

// 只能 type 使用, interface 无法使用
type ChinaMobileList = {
// 遍历属性
[phone in ChinaMobilePhones]: ChinaMobile
}

我们也可以用 keyof 来约定方法的参数

1
2
3
4
5
6
7
8
9
10
11
export type keys = {
name: string;
appId: number;
config: object;
}

class Application {
// 参数和值约束范围
set<T extends keyof keys>(key: T, val: keys[T])
get<T extends keyof keys>(key: T): keys[T]
}

img

import 导入和 export 导出

js 中有多种 importexport模块的方式,所以在 d.ts 中也对应有着不同的导入导出方式。

导入规则

在 ts 中不同的 import 语法会决定如何解析这个 module:

  • 有类型声明,标准 es module 库:import * as xxxx from ''import xxx from '' 导入
  • 有类型声明,标准 commonjs 库: import xxx = require('') 导入
  • 没有类型声明:const xxx = require('') 导入(默认导入为 any 类型)

同样,js 中不同的 export 语法也对应着不同的声明方式。

export default

如果 js 是 es6 module 风格的,通过 export default 导出的,经过编译工具编译为 export.default 以后(一般为 babel 或其他工具编译),则声明文件这样写:

js 源文件:

1
2
3
4
export default function (options) { }

// 编译后
exports.default = function (options) { }

d.ts 声明文件:

1
2
3
4
5
6
7
8
// 导出接口声明
export interface Options { }

// 声明默认函数
declare function main (options: Options): void

// 导出默认值
export default main

特点是这样引入(在 tsconfig.json 中没有开启 allowSyntheticDefaultImports 选项的情况下):

1
2
3
4
5
6
// 导入包
import * as demo from 'demo'
import { Options } from 'demo'

// 调用默认方法
demo({ })

module.exports

如果 js 是 commonjs 风格的,是通过 module.exports 导出的,则声明文件这样写:

js 源文件:

1
module.exports = function (options) { }

d.ts 声明文件:

1
2
3
4
5
6
7
8
9
10
11
// 包的声明
declare function main (options: main.Options): void

// 包里面的接口通过 namespace 声明
declare namespace main {
// 导出接口声明
export interface Options { }
}

// 导出包默认声明
export = main

特点是这样引入:

1
2
3
4
5
6
7
// 导入包
import demo = require('demo')
// 导入接口声明
import { Options } from 'demo'

// 调用默认方法
demo({ })

export xxx

如果只是普通的 exports xxx ,经过编译工具编译为 export.xxx 之后,则声明文件相对简单:

js 源文件:

1
2
3
4
export const func = (options) => { }

// 编译为
exports.func = function (options) { }

d.ts 声明文件:

1
2
3
4
// 导出接口声明
export interface Options { }

export declare const func: (options: Options) => void

引入方式:

1
2
3
4
5
// 导入接口声明
import { Options, func } from 'demo'

// 调用
func({ })

集成发布

有两种主要方式用来发布类型定义文件到 npm

  1. 与你的 npm 包捆绑在一起(内置类型定义文件)
  2. 发布到 npm 上的 @types organization

前者,安装完了包之后会自动检测并识别类型定义文件。 后者,则需要通过 npm i @types/xxxx 安装,这就是我们前面所说的 DefinitelyTyped ,用于扩展 JS 库的类型声明。

内置类型定义文件

内置类型定义就是把你的类型定义文件和 npm 包一起发布,一般来说,类型定义文件都放在包根目录的 types 目录里,例如 vue

如果你的包有一个主 .js 文件,需要在 package.json 里指定主类型定义文件。

设置 typestypeings 属性指向捆绑在一起的类型定义文件。 例如包目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
├── lib
│ ├── main.js
│ └── main.d.ts # 类型定义文件
└── package.json
// pageage.json
{
"name": "demo",
"author": "demo project",
"version": "1.0.0",
"main": "./lib/main.js",
// 定义主类型定义文件
"types": "./lib/main.d.ts"
}

如果主类型定义文件名是 index.d.ts 并且位置在包的根目录里,就不需要使用 types 属性指定了。

1
2
3
4
├── lib
│ └── main.js
├── index.d.ts # 类型定义文件
└── package.json

如果你发的包中,package.json 中使用了 files 字段的话(npm 会根据 files 配置的规则决定发布哪些文件),则需要手动把类型定义文件加入:

1
2
3
4
5
6
7
// pageage.json
{
"files": [
"index.js",
"*.d.ts"
]
}

如果只发二级目录的话,把类型定义文件放到对应的二级目录下即可:

1
import { default as App } from 'demo/app'

发布到 @types organizatio

发布到 @types organizatio 的包表示源包没有包含类型定义文件,第三方/或原作者定义好类型定义文件之后,发布到 @types 中。例如 @types/express

根据 DefinitelyTyped 的规则,和编辑器(和插件) 自动检测静态类型。

@types 下面的包是从 DefinitelyTyped 里自动发布的,通过 types-publisher 工具。

如果想让你的包发布为 @types 包,需要提交一个 pull request 到 https://github.com/DefinitelyTyped/DefinitelyTyped

在这里查看详细信息 contribution guidelines page

如果你正在使用 TypeScript,而使用了一些 JS 包并没有对应的类型定义文件,可以编写一份然后提交到 @types

赠人玫瑰,手留余香。

发布到 @types organizatio 的包可以通过 TypeSearch 搜索检索,使用 npm install --save-dev @types/xxxx 安装:

img

更多细节请参阅 DefinitelyTyped

其他

为第三方包声明类型

通常来说,如果这份类型定义文件是 JS 库自带的,那么我们可以直接导出模块:

1
2
interface User {}
export = User

而如果这份类型定义文件不是 JS 库自带的,而是第三方的,则需要使用 module 进行关联。

例如 jquery 发布的 npm 包中不包含 *.d.ts 类型定义文件,jquery 的类型定义文件发布在了 @types/jquery,所以类型定义文件中导出类型的时候,需要关联模块 jquery,意思就是我专门针对这个包做的类型定义:

1
2
3
4
5
6
interface jQuery {}
declare module 'jquery' {
// 因为 jquery 使用的是 commonjs module,所以需要用 export 导出
// es6 module 可以使用 export default 导出
export = jQuery;
}

从而解决了一些主流的 JS 库发布的 npm 包中没有类型定义文件,但是我们可以用第三方类型定义文件为这些库补充类型。

风格

经过一系列探索,个人比较推荐下面的编写风格,先看目录:

1
2
3
4
5
types
├── application.d.ts
├── config.d.ts
├── index.d.ts # 入口模块
└── user.d.ts

img

入口模块主要做这些事情:

  1. 定义命名空间
  2. 导出和聚合子模块

主出口文件 index.d.ts

1
2
3
4
5
6
7
8
9
10
import * as UserModel from './user'
import * as AppModel from './application'
import * as ConfigModel from './config'

declare namespace Models {
export type User = UserModel.User;
export type Application = AppModel.Application;
// 利用 as 抹平争议性变量名
export type Config = ConfigModel.Config;
}

子模块无需定义命名空间,这样外部环境 (types 文件夹之外) 则无法获取子模块类型,达到了类型封闭的效果:

1
2
3
4
export interface User {
name: string;
age: number
}

了解vue-class-component

vue-class-component

本文翻译自 vue-class-component

给 class 类型的 Vue 组件的 ECMAScript / TypeScript 修饰器

用法:

要求:ECMAScript 一阶段的修饰器
如果你使用 Babel,需要使用 babel-plugin-transform-decorators-legacy
如果你使用 TypeScript,请启用 –experimentalDecorators 标识

目前不支持二阶段修饰器,因为主流的编译器仍然编译旧版本的修饰器

注意

1.methods 可以直接声明作为 class 的方法

2.computed 可以直接声明作为 class 上属性的访问器

3.初始化的 data 可以声明作为 class 上的属性(如果你使用 Babel,你得使用babel-plugin-transform-class-properties

4.datarender 以及 Vue 生命周期钩子也可以直接声明作为 class 上的方法,但是你不能够不能通过当前实例引用它们(???这句不懂),当你声明一个普通的方法,你也要避免使用这些保留字。

5.对于其它的选项(options),将它们传递给修饰器函数

以下是一个 Babel 下的例子,如果你需要 TypeScript 版本,请看这里

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
36
37
38
39
40
41
42
43
<template>
<div>
<input v-model="msg">
<p>prop: {{propMessage}}</p>
<p>msg: {{msg}}</p>
<p>helloMsg: {{helloMsg}}</p>
<p>computed msg: {{computedMsg}}</p>
<button @click="greet">Greet</button>
</div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
props: {
propMessage: String
}
})
export default class App extends Vue {
// 初始化 data
msg = 123

// 使用 prop 数据初始化 data
helloMsg = 'Hello, ' + this.propMessage

// 生命周期
mounted () {
this.greet()
}

// 计算属性 computed
get computedMsg () {
return 'computed ' + this.msg
}

// method
greet () {
alert('greeting: ' + this.msg)
}
}
</script>

你可以在 vue-property-decorator 查看 @prop@watch 修饰器

使用 Mixins

vue-class-component 提供 mixins 帮助函数,可以用来在 class 类型风格中使用 mixins
通过 mixins 帮助函数,Typescript 可以推断出 mixin 类型并且在组件类型中继承它们。

声明 mixin 的例子:

1
2
3
4
5
6
7
8
9
// mixin.js
import Vue from 'vue'
import Component from 'vue-class-component'

// 你可以想声明一个组件一样声明一个 mixin
@Component
export default class MyMixin extends Vue {
mixinValue = 'Hello'
}

然后使用它:

1
2
3
4
5
6
7
8
9
10
11
12
import Component, { mixins } from 'vue-class-component'
import MyMixin from './mixin.js'

// 使用 `mixins` 帮助函数,而不是 `Vue`.
// `mixins` 可以获取任何数量的参数

@Component
export class MyComp extends mixins(MyMixin) {
created () {
console.log(this.mixinValue) // -> Hello
}
}

自定义修饰器

你还可以创建你自己的修饰器并且继承这个库的功能,vue-class-component 提供 createDecorator 帮助函数用来创建自定义修饰器。

createDecorator 的第一个参数是一个回调函数,并且这个函数可以获取一下参数:

options: vue 组件选项组成的对象,改变这些选项会影响所提供的组件
key: 修饰器所作为的属性或方法的 key
parameterIndex: 修饰器作用于参数时,这个参数的索引

例子:创建一个 NoCache 修饰器

1
2
3
4
5
6
7
8
9
// decorators.js
import { createDecorator } from 'vue-class-component'

export const NoCache = createDecorator((options, key) => {
// 组件的选项应该传给回调函数,同时会更新选项对象(options object)
// 进而作用于组件
options.computed[key].cache = false
})
import { NoCache } from './decorators'
1
2
3
4
5
6
7
8
@Component
class MyComp extends Vue {
// 这个计算属性不会被缓存
@NoCache
get random () {
return Math.random()
}
}

添加自定义钩子

如果你使用了一些Vue 插件比如 Vue Router,你可能会希望 class 组件解析它们所提供的钩子,比如,下面的例子中 Component.registerHooks 就允许你注册这些钩子

1
2
3
4
5
6
7
8
9
// class-component-hooks.js
import Component from 'vue-class-component'

// 通过这些钩子的名称来注册它们
Component.registerHooks([
'beforeRouteEnter',
'beforeRouteLeave',
'beforeRouteUpdate' // for vue-router 2.2+
])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MyComp.js
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
class MyComp extends Vue {
// class 组件现在可以处理 beforeRouteEnter 钩子和
// beforeRouteLeave 钩子作为 Vue Router 钩子
beforeRouteEnter (to, from, next) {
console.log('beforeRouteEnter')
next() // 需要调用这个来确认导航
}

beforeRouteLeave (to, from, next) {
console.log('beforeRouteLeave')
next() // 需要调用这个来确认导航
}
}

值得注意的是,你必须在组件定义之前注册钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.js

// 确保在引入任何组件之前注册
import './class-component-hooks'

import Vue from 'vue'
import MyComp from './MyComp'

new Vue({
el: '#app',
components: {
MyComp
}
})

注意事项

vue-class-component 通过实例化 钩子下的初始构造函数来收集 class 属性作为 Vue 实例的 data(???这句不太懂)。然而我们也可以像本地 class 的方式定义实例 data,有时候我们需要知道它是如何工作的。

this

如果你定义一个 class 属性并且在里面访问 this,它不会起作用,因为 this 只是当我们实例化 class 属性时候 Vue 实例的一个拦截对象。

1
2
3
4
5
6
7
8
9
10
@Component
class MyComp extends Vue {
foo = 123

bar = () => {
// 不会如预期中的更新
// `this` 的值实际上不是 Vue 实例
this.foo = 456
}
}

你可以简单地定义一个方法而不是一个 class 属性因为 Vue 会自动绑定实例

1
2
3
4
5
6
7
8
9
@Component
class MyComp extends Vue {
foo = 123

bar () {
// 如预期地更新数据
this.foo = 456
}
}
undefined 不会响应式

为了在 Babel 和 TypeScript 上表现稳定,如果一个属性的初始化值是 undefined,vue-class-components 不会对它触发响应式,你应该使用 null 作为 初始值货值使用 data 钩子来初始化值为 undefined 的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
class MyComp extends Vue {
// 没有响应式
foo = undefined

// 响应式
bar = null

data () {
return {
// 响应式
baz: undefined
}
}
}
创建例子
1
$ npm install && npm run example

Vue 面试题

1、说说你对 SPA 单页面的理解,它的优缺点分别是什么?

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

2、v-show 与 v-if 有什么区别?

v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

3、Class 与 Style 如何动态绑定?

Class 可以通过对象语法和数组语法进行动态绑定:

  • 对象语法:
1
2
3
4
5
6
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
isActive: true,
hasError: false
}
  • 数组语法:
1
2
3
4
5
6
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
activeClass: 'active',
errorClass: 'text-danger'
}

Style 也可以通过对象语法和数组语法进行动态绑定:

  • 对象语法:
1
2
3
4
5
6
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
activeColor: 'red',
fontSize: 30
}
  • 数组语法:
1
2
3
4
5
6
7
8
9
10
<div v-bind:style="[styleColor, styleSize]"></div>

data: {
styleColor: {
color: 'red'
},
styleSize:{
fontSize:'23px'
}
}

4、怎样理解 Vue 的单向数据流?

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。

这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

有两种常见的试图改变一个 prop 的情形 :

  • 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
1
2
3
4
5
6
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
  • 这个 prop 以一种原始的值传入且需要进行转换**。** 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
1
2
3
4
5
6
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}

5、computed 和 watch 的区别和运用的场景?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

6、直接给一个数组项赋值,Vue 能检测到变化吗?

由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

为了解决第一个问题,Vue 提供了以下操作方法:

1
2
3
4
5
6
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

为了解决第二个问题,Vue 提供了以下操作方法:

1
2
// Array.prototype.splice
vm.items.splice(newLength)

7、谈谈你对 Vue 生命周期的理解?

(1)生命周期是什么?

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

(2)各个生命周期的作用

生命周期 描述
beforeCreate 组件实例被创建之初,组件的属性生效之前
created 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update 组件数据更新之后
activited keep-alive 专属,组件被激活时调用
deadctivated keep-alive 专属,组件被销毁时调用
beforeDestory 组件销毁前调用
destoryed 组件销毁后调用

(3)生命周期示意图

img

8、Vue 的父组件和子组件生命周期钩子函数执行顺序?

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程

父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程

父 beforeUpdate -> 父 updated

  • 销毁过程

父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

9、在哪个生命周期内调用异步请求?

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

10、在什么阶段才能访问操作DOM?

在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。

vue 具体的生命周期示意图可以参见如下,理解了整个生命周期各个阶段的操作,关于生命周期相关的面试题就难不倒你了。

img

11、父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

1
2
3
4
5
6
7
// Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},

// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。

12、谈谈你对 keep-alive 的了解?

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

13、组件中 data 为什么是一个函数?

为什么组件中的 data 必须是一个函数,然后 return 一个对象,而 new Vue 实例里,data 可以直接是一个对象?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// data
data() {
return {
message: "子组件",
childName:this.name
}
}

// new Vue
new Vue({
el: '#app',
router,
template: '<App/>',
components: {App}
})

因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,

如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。

14、v-model 的原理?

我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  • text 和 textarea 元素使用 value 属性和 input 事件;
  • checkbox 和 radio 使用 checked 属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

以 input 表单元素为例:

1
2
3
4
5
<input v-model='something'>

相当于

<input v-bind:value="something" v-on:input="something = $event.target.value">

如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
父组件:
<ModelChild v-model="message"></ModelChild>

子组件:
<div>{{value}}</div>

props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},

15、Vue 组件间通信有哪几种方式?

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。

Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。

(1)props / $emit 适用 父子组件通信

这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。

(2)ref$parent / $children 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
  • $parent / $children:访问父 / 子实例

(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

(4)$attrs/$listeners 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

(5)provide / inject 适用于 隔代组件通信

祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

(6)Vuex 适用于 父子、隔代、兄弟组件通信

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

16、你使用过 Vuex 吗?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

(1)Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

主要包括以下几个模块:

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

17、使用过 Vue SSR 吗?说说 SSR?

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记”激活”为客户端上完全可交互的应用程序。

即:SSR大致的意思就是vue在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。

服务端渲染 SSR 的优缺点如下:

(1)服务端渲染的优点:

  • 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
  • 更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

(2) 服务端渲染的缺点:

  • 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
  • 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

18、vue-router 路由模式有几种?

vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}

其中,3 种路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

19、能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?

(1)hash 模式的实现原理

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 ‘#search’:

1
https://www.word.com#search

hash 路由模式的实现主要是基于下面几个特性:

  • URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。

(2)history 模式的实现原理

HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。

唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

1
2
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

history 路由模式的实现主要基于存在下面几个特性:

  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
  • history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

20、什么是 MVVM?

Model–View–ViewModel (MVVM) 是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。

由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表

MVVM 源自于经典的 Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。

如下图所示:

img

(1)View 层

View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。

(2)Model 层

Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。

(3)ViewModel 层

ViewModel 是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。

需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。

MVVM 框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。

这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现,有 Vue 开发经验的同学应该一目了然:

(1)View 层

1
2
3
4
<div id="app">
<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>
</div>

(2)ViewModel 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var app = new Vue({
el: '#app',
data: { // 用于描述视图状态
message: 'Hello Vue!',
},
methods: { // 用于描述视图行为
showMessage(){
let vm = this;
alert(vm.message);
}
},
created(){
let vm = this;
// Ajax 获取 Model 层的数据
ajax({
url: '/your/server/data/api',
success(res){
vm.message = res;
}
});
}
})

(3) Model 层

1
2
3
4
5
6
7
8
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "IoveC",
"domain": "www.cnblogs.com"
}
}

21、Vue 是如何实现数据双向绑定的?

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:

img

即:

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。

其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。

Vue 主要通过以下 4 个步骤来实现数据双向绑定的:

实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。

实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

以上四个步骤的流程图表示如下,如果有同学理解不大清晰的,可以查看作者专门介绍数据双向绑定的文章《0 到 1 掌握:Vue 核心之数据双向绑定》,有进行详细的讲解、以及代码 demo 示例。

img

22、Vue 框架怎么实现对象和数组的监听?

如果被问到 Vue 怎么实现数据双向绑定,大家肯定都会回答 通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持。

同理无法对数组进行劫持,但是我们在使用 Vue 框架中都知道,Vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // observe 功能为监测数据的变化
}
}

/**
* 对属性进行递归遍历
*/
let childOb = !shallow && observe(val) // observe 功能为监测数据的变化

通过以上 Vue 源码部分查看,我们就能知道 Vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty() 也能对对象和数组(部分方法的操作)进行监听。

23、Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:

  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

Object.defineProperty 的优势如下:

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

24、Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)来实现为对象添加响应式属性,那框架本身是如何实现的呢?

我们查看对应的 Vue 源码:vue/src/core/instance/index.js

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
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}

我们阅读以上源码可知,vm.$set 的实现原理是:

  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;
  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

25、虚拟 DOM 的优缺点?

优点:

  • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

26、虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

27、Vue 中的 key 有什么作用?

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。

Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。

具体有无 key 的 diff 过程,可以查看作者写的另一篇详解虚拟 DOM 的文章《深入剖析:Vue核心之虚拟DOM》

所以 Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:

1
2
3
4
5
6
7
8
9
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}

28、你有对 Vue 项目进行哪些优化?

如果没有对 Vue 项目没有进行过优化总结的同学,可以参考本文作者的另一篇文章《 Vue 项目性能优化 — 实践指南 》,文章主要介绍从 3 个大方面,22 个小方面详细讲解如何进行 Vue 项目的优化。

(1)代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化

(3)基础的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

29、对于即将到来的 vue3.0 特性你有什么了解的吗?

Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:

(1)监测机制的改变

3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:

  • 只能监测属性,不能监测对象
  • 检测属性的添加和删除;
  • 检测数组索引和长度的变更;
  • 支持 Map、Set、WeakMap 和 WeakSet。

新的 observer 还提供了以下特性:

  • 用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
  • 默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
  • 更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
  • 不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
  • 更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2)模板

模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3)对象式的组件声明方式

vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。

3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。

此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。

现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4)其它方面的更改

vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:

  • 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
  • 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
  • 基于 treeshaking 优化,提供了更多的内置功能。

30、说说你使用 Vue 框架踩过最大的坑是什么?怎么解决的?

本题为开放题目,欢迎大家在评论区畅所欲言,分享自己的踩坑、填坑经历,提供前车之鉴,避免大伙再次踩坑 ~

实现跨域存储

本地存储受同源策略限制

客户端(浏览器)出于安全性考虑,无论是 localStorage 还是 sessionStorage 都会受到同源策略限制。

那么如何实现跨域存储呢?

window.postMessage()

想要实现跨域存储,先找到一种可跨域通信的机制,没错,就是 postMessage,它可以安全的实现跨域通信,不受同源策略限制。

语法:

1
otherWindow.postMessage('message', targetOrigin, [transfer])
  • otherWindow 窗口的一个引用,如:iframecontentWindow 属性,当前 window 对象,window.open 返回的窗口对象等
  • message 将要发送到 otherWindow 的数据
  • targetOrigin 通过窗口的 targetOrigin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 "*"(表示无限制)
实现思路

postMessage 可跨域特性,来实现跨域存储。因为多个不同域下的页面无法共享本地存储数据,我们需要找个“中转页面”来统一处理其它页面的存储数据。为了方便理解,画了张时序图,如下:

img

​ 跨域存储时序图

场景模拟

需求:

有两个不同的域名(http://localhost:6001http://localhost:6002)想共用本地存储中的同一个 token 作为统一登录凭证:

假设:

http://localhost:6001 对应 client1.html 页面

http://localhost:6002 对应 client2.html 页面

http://localhost:6003 对应 hub.html 中转页面

启动服务:

使用 http-server 启动 3 个本地服务

1
2
3
4
5
6
npm -g install http-server

# 启动 3 个不同端口的服务,模拟跨域现象
http-server -p 6001
http-server -p 6002
http-server -p 6003
简单实现版本

client1.html 页面代码

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
<body>
<!-- 开始存储事件 -->
<button onclick="handleSetItem()">client1-setItem</button>
<!-- iframe 嵌套“中转页面” hub.html -->
<iframe src="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

<script>
const $ = id => document.querySelector(id)
// 获取 iframe window 对象
const ifameWin = $('#hub').contentWindow

let count = 0
function handleSetItem () {
let request = {
// 存储的方法
method: 'setItem',
// 存储的 key
key: 'someKey',
// 需要存储的数据值
value: `来自 client-1 消息:${count++}`,
}
// 向 iframe “中转页面”发送消息
ifameWin.postMessage(request, '*')
}
</script>
</body>

hub.html 中转页面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<script>
// 映射关系
let map = {
setItem: (key, value) => window.localStorage['setItem'](key, value "'setItem'"),
getItem: (key) => window.localStorage['getItem'](key "'getItem'"),
}

// “中转页面”监听 ifameWin.postMessage() 事件
window.addEventListener('message', function (e) {
let { method, key, value } = e.data
// 处理对应的存储方法
let result = map[method](key, value "method")
// 返回给当前 client 的数据
let response = {
result,
}
// 把获取的数据,传递给 client 窗口
window.parent.postMessage(response, '*')
})
</script>
</body>

client2.html 页面代码

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
<body>
<!-- 获取本地存储数据 -->
<button onclick="handleGetItem()">client2-getItem</button>
<!-- iframe 嵌套“中转页面” hub.html -->
<iframe src="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

<script>
const $ = id => document.querySelector(id)
// 获取 iframe window 对象
const ifameWin = $('#hub').contentWindow

function handleGetItem () {
let request = {
// 存储的方法(获取)
method: 'getItem',
// 获取的 key
key: 'someKey',
}
// 向 iframe “中转页面”发送消息
ifameWin.postMessage(request, '*')
}

// 监听 iframe “中转页面”返回的消息
window.addEventListener('message', function (e) {
console.log('client 2 获取到数据啦:', e.data)
})
</script>
</body>

浏览器打开如下地址:

改进版本

共拆分成 2 个 js 文件,一个是客户端页面使用 client.js,另一个是中转页面使用 hub.js,具体代码如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// client.js

class Client {
constructor (hubUrl) {
this.hubUrl = hubUrl
// 每个请求的 id 值,作为唯一标识(累加)
this.id = 0
// 所有请求消息数据映射(如:getItem、setItem)
this._requests = {}
// 获取 iframe window 对象
this._iframeWin = this._createIframe(this.hubUrl).contentWindow
this._initListener()
}
// 获取存储数据
getItem (key, callback) {
this._requestFn('getItem', {
key,
callback,
})
}
// 更新存储数据
setItem (key, value, callback) {
this._requestFn('setItem', {
key,
value,
callback,
})
}
_requestFn (method, { key, value, callback }) {
// 发消息时,请求对象格式
let req = {
id: this.id++,
method,
key,
value,
}
// 请求唯一标识 id 和回调函数的映射
this._requests[req.id] = callback
// 向 iframe “中转页面”发送消息
this._iframeWin.postMessage(req, '*')
}
// 初始化监听函数
_initListener () {
// 监听 iframe “中转页面”返回的消息
window.addEventListener('message', (e) => {
let { id, result } = e.data
// 找到“中转页面”的消息对应的回调函数
let currentCallback = this._requests[id]
if (!currentCallback) return
// 调用并返回数据
currentCallback(result)
})
}
// 创建 iframe 标签
_createIframe (hubUrl) {
const iframe = document.createElement('iframe')
iframe.src = hubUrl
iframe.style = 'display: none;'
window.document.body.appendChild(iframe)
return iframe
}
}
// hub.js

class Hub {
constructor () {
this._initListener()
this.map = {
setItem: (key, value) => window.localStorage['setItem'](key, value "'setItem'"),
getItem: (key) => window.localStorage['getItem'](key "'getItem'"),
}
}
// 监听 client ifameWin.postMessage() 事件
_initListener () {
window.addEventListener('message', (e) => {
let { method, key, value, id } = e.data
// 处理对应的存储方法
let result = this.map[method](key, value "method")
// 返回给当前 client 的数据
let response = {
id,
result,
}
// 把获取的数据,发送给 client 窗口
window.parent.postMessage(response, '*')
})
}
}

页面使用:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!-- client1 页面代码 -->

<body>
<button onclick="handleGetItem()">client1-GetItem</button>
<button onclick="handleSetItem()">client1-SetItem</button>

<script src="./lib/client.js"></script>
<script>
const crossStorage = new Client('http://localhost:6003/hub.html')
// 在 client1 中,获取 client2 存储的数据
function handleGetItem () {
crossStorage.getItem('client2Key', (result) => {
console.log('client-1 getItem result: ', result)
})
}

// client1 本地存储
function handleSetItem () {
crossStorage.setItem('client1Key', 'client-1 value', (result) => {
console.log('client-1 完成本地存储')
})
}
</script>
</body>
<!-- hub 页面代码 -->

<body>
<script src="./lib/hub.js"></script>
<script>
const hub = new Hub()
</script>
</body>
<!-- client2 页面代码 -->

<body>
<button onclick="handleGetItem()">client2-GetItem</button>
<button onclick="handleSetItem()">client2-SetItem</button>

<script src="./lib/client.js"></script>
<script>
const crossStorage = new Client('http://localhost:6003/hub.html')
// 在 client2 中,获取 client1 存储的数据
function handleGetItem () {
crossStorage.getItem('client1Key', (result) => {
console.log('client-2 getItem result: ', result)
})
}
// client2 本地存储
function handleSetItem () {
crossStorage.setItem('client2Key', 'client-2 value', (result) => {
console.log('client-2 完成本地存储')
})
}
</script>
</body>
总结

以上就实现了跨域存储,也是 cross-storage 开源库的核心原理。通过 window.postMessage() api 跨域特性,再配合一个 “中转页面”,来完成所谓的“跨域存储”,实际上并没有真正的在浏览器端实现跨域存储,这是浏览器的限制,我们无法打破,只能用“曲线救国”的方式,变向来共享存储数据。

Git的41 个 问答

1. 你最喜欢的 Git 命令是什么

个人比较喜欢 git add -p. 这增加了“补丁模式”的变化,这是一个内置的命令行程序。它遍历了每个更改,并要求确认是否要执行它们。

这个命令迫使咱们放慢速度并检查更改文件。作为开发人员,咱们有时常常急于提交,我自己也经常这样,做完运行 git add . 才发现把调试的代码也提交上去了。

2. 为什么你更喜欢直接使用 git 命令

作为开发人员,咱们也经常使用其它命令来做其它事情,也不差用 git 的命令来做事。

此外,git 命令也是非常短的,非常容易学习,并且使用命令可以了解 git 的工作流程,这样也间接改进了开发工作流程。

3. 如何使用 stage 命令

stageadd .的内置别名。

4.如何在分支中保存更改并 checkout 到其他分支

因此,可以使用 git stash 临时存储更改或提交 WIP,目的是要有未修改前的环境。就我个人而言,我更喜欢使用 WIP 提交而不是 stash,因为它们更容易引用和共享。

WIP = Work in Progress

研发中的代码想存储起来,但是又避免研发中的代码被合并,开发就会创建一个WIP的分支

WIP MR

WIP MR 含义是 在工作过程中的合并请求,是一个我们在 GitLab 中避免 MR 在准备就绪前被合并的技术。只需要添加 WIP: 在 MR 的标题开头,它将不会被合并,除非你把 WIP: 删除。

5.什么时候使用 git stash

发现有一个类是多余的,想删掉它又担心以后需要查看它的代码,想保存它但又不想增加一个脏的提交。这时就可以考虑 git stash

6.如何使用 git 命令

对任何命令使用 --help选项,例如,git stash --help

7. 什么是“ git flow”?

Git Flow 定义了一个项目发布的分支模型,为管理具有预定发布周期的大型项目提供了一个健壮的框架,是由 Vincent Driessen 提出的一个 git 操作流程标准、解决当分支过多时 , 如何有效快速管理这些分支。

8.什么是 GitHub flow ?

GitHub flow,顾名思义,就是 GitHub 所推崇的 Workflow。(千万不要理解成 GitHub 上才能用的 Workflow), 基本上,GitHub Flow 是master/feature分支工作流程的品牌名称。

GitHub flow 的核心优势在于其流程带来的自动化可能性,能够做到其它流程无法实现的检查过程,并极大简化开发团队的体力劳动,真正发挥自身的价值。

9.你更喜欢哪种分支策略?

大多数 Git项目都是 “Git flow”。这些项目中只有少数需要这种策略,通常是因为它是版本化的软件。

master/feature 分支策略更易于管理,尤其是在刚入门时,如果需要,切换到 “git flow” 非常容易。

10. git open 命令是做啥用的

这是一个单独的命令,可以作为 npm 包使用。

11.当在其他分支中添加的文件仍然在工作分支中显示为未跟踪或修改时,如何重置分支

这通常是“工作索引”不干净时切换分支的结果。

在 git 中没有内置的方法来纠正这一点。通常通过确保提示符有一个 “status” 指示符并在每次更改分支时运行诸如 git status 之类的命令来避免这种情况。这些习惯会让咱们尽早发现这些问题,这样就可以在新的分支上 stashcommit 这些更改。

12. 如何重命名分支?

1
git branch -m current-branch-name new-branch-name

13. 如何使用 cherry-pick

git cherry-pick [reference] 请记住,这是一个重新应用的命令,因此它将更改提交 SHA。

14. 如果从一个分支恢复(例如 HEAD~3),是否可以再次返回到 HEAD(比如恢复上一次更新)

在这种情况下,通过运行 git reset --hard HEAD~1 立即撤消还原提交(即 HEAD 提交)。

15. 什么时候使用 git pullgit fetch

git pull将下载提交到当前分支。记住,git pull实际上是 fetchmerge 命令的组合。

git fetch将从远程获取最新的引用。

一个很好的类比是播客播放器或电子邮件客户端。咱们可能会检索最新的播客或电子邮件(fetch),但实际上尚未在本地下载播客或电子邮件附件(pull)。

16. 为什么有时需要使用 --force 来强制提交更改

rebase 是一个可以重新提交的命令,它改变了 SHA1 hash。如果是这样,本地提交历史将不再与其远程分支保持一致。

当这种情况发生时,push 会被拒绝。只有在被拒绝时,才应该考虑使用 git push --force。这样做将用本地提交历>史覆盖远程提交历史。所以可以回过头来想想,想想为什么要使用 --force

17. 可以使用分支合并多个分支,然后将该分支发送给 master 吗?

当然可以,在大多数 git 工作流下,分支通常会累积来自多个其他分支的更改,最终这些分支会被合并到主分支。

18. 应该从一个非常老的分支做一个 rebase 吗?

除非是迫不得已。

根据你的工作流,可以将旧的分支合并到主分支中。

如果你需要一个最新的分支,我更喜欢 rebase。它只提供更改且更清晰的历史记录,而不是来自其他分支或合并的提交。

然而,尽管总是可能的,但是使用 rebase 可能是一个痛苦的过程,因为每次提交都要重新应用。这可能会导致多重冲突。如果是这样,我通常使用rebase --abort 并使用 merge 来一次性解决所有冲突。

19. 使用 rebase -i 时,squashfixup 有什么区别

squash 和 fixup 结合两个提交。squash 暂停 rebase 进程,并允许咱们调整提交的消息。fixup 自动使用来自第一次提交的消息。

20. 通常,当使用 master 重新建立功能分支时,对于每次提交都需要解决冲突?

是的。由于每次提交的更改都会在 rebase 期间重新应用,所以必须在冲突发生时解决它们。

这意味着在提交之前就已经有了提交冲突,如果没有正确地解决它,那么下面的许多提交也可能发生冲突。为了限制这一点,我经常使用 rebase -i 来压缩提交历史记录,以便更轻松地使用它。

如果许多提交之间仍然存在冲突,可以使用 merge

21.在与 master 合并之前,有必要更新我的分支吗

根据你的工作流,可以将旧的分支合并到主分支中。如果你的工作流仅使用 “fast-forward“合并,那么有必要在合并之前更新你的分支。

Git fast forward 提交

多人协同开发,使用 Git 经常会看到警告信息包含术语:fast forward, 这是何义?

简单来说就是提交到远程中心仓库的代码必须是按照时间顺序的。比如 A 从中心仓库拿到代码后,对文件 f 进行了修改。然后 push 到中心仓库。

BA 之前就拿到了中心仓库的代码,在 A push 成功之后也对 f 文件进行了修改。这个时候 B 也运行 push 命令推送代码。

会收到一个类似下面的信息:

1
2
3
4
5
6
chenshu@sloop2:~/work/189/appengine$ git pushTo 
ssh://csfreebird@10.112.18.189:29418/appengine.git ! [rejected]
master -> master (non-fast-forward)error: failed to push some refs to
'ssh://csfreebird@10.112.18.189:29418/appengine.git'To prevent you from losing
history, non-fast-forward updates were rejectedMerge the remote changes (e.g. 'git
pull') before pushing again. See the'Note about fast-forwards' section of 'git push --help' for details.

提醒你非快进方式的更新被拒绝了,需要先从中心仓库pull到最新版本,merge后再 push.

fast forward 能够保证不会强制覆盖别人的代码,确保了多人协同开发。尽量不要使用 non fast forward方法提交代码。

22. 需要使用 GitKraken 这种可视化工具吗

我比较喜欢用命令方式使用 git,因为这使我能够完全控制管理变更,就像使用命令来改进我的开发过程一样。

当然,某些可视化操作(如管理分支和查看文件差异)在GUI中总是更好。我个人认为在合并过程中在浏览器中查看这些内容就足够了。

23. 当提交已经被推送时,可以做一个 --amend 修改吗?

可以,git commit –amend 既可以对上次提交的内容进行修改,也可以修改提交说明。

24.在做迭代内容时,当完成一个小功能需要先拉一个 pull request 请求,还是都做完这个迭代内容后在拉一个 pull request 请求

咱们通常做法是,完成一个迭代的内容后在拉一个 pull request。然而,如果你某个任务上花了很长时间,先合并做的功能可能是有益的。这样做可以防止对分支的依赖或过时,所以做完一个拉一个请求,还是全部做完在拉一个请求,这决于你正在进行的更改的类型。

25. 在将分支合并到 master 之前,需要先创建一个 release 分支吗?

这在很大程度上取决于你们公司的部署过程。创建 release 分支对于将多个分支的工作分组在一起并将它们合并到主分支之前进行整体测试是有益的。

由于源分支保持独立和未合并,所以在最后的合并中拥有更大的灵活性。

26. 如何从 master 获取一些提交?比方说,我不想执行最后一次提交,而是进行一次 rebase

假设 master 分支是咱们的主分支,咱们不希望有选择地从它的历史记录中提取提交,这会以后引起冲突。

咱们想要 mergerebase 分支的所有更改。要从主分支之外的分支提取选择提交,可以使用 git cherry-pick

27. 如何在 git 终端配置颜色

默认情况 下git 是黑白的。

1
2
3
4
git config --global color.status auto 
git config --global color.diff auto
git config --global color.branch auto
git config --global color.interactive auto

配置之后,就有颜色了。

28. 有没有更好的命令来替代 git push -force ?

实际上,没有其他方法可以替代 git push—force。虽然这样,如果正确地使用 mergerebase 更新分支,则无需使用 git push --force

只有当你运行了更改本地提交历史的命令时,才应该使用 git push --force

29. 当我在 git rebase - 选择drop时,是否删除了与该提交相关的代码?

是的。要恢复这段代码,需要在 reflogrebase 之前找到一个状态。

30. 如何自动跟踪远程分支

通常,当你 checkout 或创建分支时,Git 会自动设置分支跟踪。

如果没有,则可以在下一次使用以下命令进行更新时:git push -u remote-name branch-name

或者可以使用以下命令显式设置它:git branch --set-upstream-to = remote-name / branch-name

31. 在 rebase 分支之前更新分支,是一个好的习惯吗?

我认为是这样的,原因很简单,用git rebase -i 组织或压缩提交,首先在更新过程中提供更多的上下文。

32. 有没有一种方法可以将提交拆分为更多的提交(与 fixup/squash 相反)?

可以在rebase -i过程中使用 exec 命令来尝试修改工作索引并拆分更改。还可以使用 git reset 来撤消最近的提交,并将它们的更改放入工作索引中,然后将它们的更改分离到新的提交中。

33.有没有办法查看已修复的提交?

git log

查看日志,找到对应的修改记录,但是这种查找只能看到文件,而不是文件的内容。

git blame 文件名

查看这个文件的修改记录,默认显示整个文件,也可以通过参数 -L,来检查需要修改的某几行。

如果查看之前提交的内容可以使用 git show commitId

34. rebase –skip 作用是啥?

咱们知道 rebase 的过程首先会产生 rebase 分支(master)的备份,放到(no branch )临时分支中。再将支线分支(branch)的每一次提交修改,以补丁的形式,一个个的重新应用到主干分支上。这个过程是一个循环应用补丁的过程,期间只要补丁产生冲突,就会停止循环,等待手动解决冲突。这个冲突指的是上一个合并后版本与补丁之间的冲突。

git rebase --skip 命令,可以跳过某一次补丁(存在上一轮冲突的解决方案中,已经包含了这一轮的补丁内容,这样会使补丁无效,需要跳过),这个命令慎用。

35. 如何删除远程分支?

可以使用:git push origin:branch-name-to-remove 或使用 -d选项:git push -d origin someother -branch-2 来删除远程分支。

要删除对远程分支的本地引用,可以运行:git remote prune origin

36. checkoutreset 有什么区别

这两个命令都可以用来撤销更改。checkout 可能更健壮,因为它不仅允许撤消当前更改,而且还允许通过检索文件的旧版本撤消一组更改。

默认情况下,reset更适合于更改工作索引中更改的状态。因此,它实际上只处理当前的变化。

git checkout -- file;撤销对工作区修改;这个命令是以最新的存储时间节点(add和commit)为参照,覆盖工作区对应文件file;这个命令改变的是工作区。

git reset HEAD -- file;清空 add 命令向暂存区提交的关于 file 文件的修改(Ustage);这个命令仅改变暂存区,并不改变工作区,这意味着在无任何其他操作的情况下,工作区中的实际文件同该命令运行之前无任何变化

37. 在正常的工作流程中应该避免使用哪些命令

一些可能会破坏历史记录的内容,例如:

1
2
3
git push origin master -f (千万不要这样做)
git revert
git cherry-pick (changes from master)

在正常的工作流程下,尽量避免直接使用git merge,因为这通常是通过拉请求(pull requests)构建到流程中的。

38. 如果我有一个分支(B)指向另一个分支(A),而我又有另一个分支(C),它需要(A)和(B)及 mast 分支的代码,怎么个流程才能更新(C)?

这取决于几件事:

如果 AB 可以合并到 master,则可以将 AB 合并到 master 中,然后用master的更新 C

如果 AB 不能合并到 master,可以简单地将 B 合并到 C 中,因为 B 已经包含了 A 的变更。

在极端的情况下,可以将 ABmaster 合并到 C 中。然而,为了避免冲突,合并的顺序可能很重要。

39. 你使用的别名有哪些

我常用的一些 git 别名如下:

1
2
3
4
5
alias.unstage reset HEAD --
alias.append commit --amend --no-edit
alias.wip commit -m "WIP"
alias.logo log --oneline
alias.lola log --graph --oneline --decorate --all

40. 鲜为人知的 git 命令有哪些?

git bisect 是查找代码中存在的bug的救命工具。虽然只使用过几次,但它的精确度令人印象深刻,节省了大量时间。

git archive 是用于打包一组更改的好工具。这有助于与第三方或 mico-deployment 共享工作。

git reflog 可能是众所周知的,但值得一提,因为它提供了一种在出错时“撤消”命令的好方法。

41. 你能推荐一些关于Git的书籍吗

我建议至少阅读Pro Git的前三章。这些年来,每看到一遍,或多或少都有收获。

也不错。

数组去重

双层 for 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
function distinct(arr) {
for (let i=0, len=arr.length; i<len; i++) {
for (let j=i+1; j<len; j++) {
if (arr[i] == arr[j]) {
arr.splice(j, 1);
// splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一
len--;
j--;
}
}
}
return arr;
}

思想: 双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,效率会很低

Array.filter() 加 indexOf

1
2
3
4
5
6
function distinct(a, b) {
let arr = a.concat(b);
return arr.filter((item, index)=> {
return arr.indexOf(item) === index
})
}

思想: 利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素

Array.sort() 加一行遍历冒泡(相邻元素去重)

1
2
3
4
5
6
7
8
9
10
11
12
13
function distinct(array) {
var res = [];
var sortedArray = array.concat().sort();
var seen;
for (var i = 0, len = sortedArray.length; i < len; i++) {
// 如果是第一个元素或者相邻的元素不相同
if (!i || seen !== sortedArray[i]) {
res.push(sortedArray[i])
}
seen = sortedArray[i];
}
return res;
}

思想: 调用了数组的排序方法 sort(),V8引擎 的 sort() 方法在数组长度小于等于10的情况下,会使用插入排序,大于10的情况下会使用快速排序(sort函数在我之前高阶函数那篇文章有详细讲解)。然后根据排序后的结果进行遍历及相邻元素比对(其实就是一行冒泡排序比较),如果相等则跳过该元素,直到遍历结束。

ES6 中的 Set 去重

1
2
3
function distinct(array) {
return Array.from(new Set(array));
}

甚至可以再简化下:

1
2
3
function unique(array) {
return [...new Set(array)];
}

还可以再简化下:

1
let unique = (a) => [...new Set(a)]

思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是唯一的,没有重复的值。(同时请大家注意这个简化过程)

Object 键值对

1
2
3
4
5
6
function distinct(array) {
var obj = {};
return array.filter(function(item, index, array){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}

这种方法是利用一个空的 Object 对象,我们把数组的值存成 Object 的 key 值,比如 Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的,但是最后请注意这里obj[typeof item + item] = true没有直接使用obj[item],是因为 123 和 ‘123’ 是不同的,直接使用前面的方法会判断为同一个值,因为对象的键值只能是字符串,所以我们可以使用 typeof item + item 拼成字符串作为 key 值来避免这个问题。

性能考虑(是想要最快的速度查到数据吗?)

为了测试这些解法的性能,我写了一个测试模版,用来计算数组去重的耗时。模版代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// distinct.js

let arr1 = Array.from(new Array(100000), (x, index)=>{
return index
})

let arr2 = Array.from(new Array(50000), (x, index)=>{
return index+index
})

let start = new Date().getTime()
console.log('开始数组去重')

let arr = a.concat(b);

function distinct(arr) {
// 数组去重
}

console.log('去重后的长度', distinct(arr).length)

let end = new Date().getTime()
console.log('耗时', end - start)

上面的多种数组去后,计算耗费时间

双重 for 循环 > Array.filter()加 indexOf > Array.sort() 加一行遍历冒泡 > ES6中的Set去重 > Object 键值对去重复

兼容性与场景考虑(数组中是否包含对象,NaN等?)

我们要考虑这个数组中是否有null、undefined、NaN、对象如果二者都出现,上面的所有数组去重方法并不是都是适用哦,下面详细说一下。

先说一下 == 和 === 区别

=== 严格相等,会比较两个值的类型和值== 抽象相等,比较时,会先进行类型转换,然后再比较值 想更详细了解转换过程的可以看这篇文章js 中 == 和 === 的区别

说一下我说的几个类型的相等问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let str1 = '123';
let str2 = new String('123');

console.log(str1 == str2); // true
console.log(str1 === str2); // false

console.log(null == null); // true
console.log(null === null); // true

console.log(undefined == undefined); // true
console.log(undefined === undefined); // true

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false

console.log(/a/ == /a/); // false
console.log(/a/ === /a/); // false

console.log({} == {}); // false
console.log({} === {}); // false

几种去重函数针对带有特殊类型的对比

indexOf 与 Set 的一点说明

上面代码中console.log(NaN === NaN); // false, indexOf 底层使用的是 === 进行判断,所以使用 indexOf 查找不到 NaN 元素

1
2
3
// demo1
var arr = [1, 2, NaN];
arr.indexOf(NaN); // -1

Set可以去重NaN类型, Set内部认为尽管 NaN === NaN 为 false,但是这两个元素是重复的。

1
2
3
4
5
// demo2
function distinct(array) {
return Array.from(new Set(array));
}
console.log(unique([NaN, NaN])) // [NaN]

具体去重比较

将这样一个数组按照上面的方法去重后的比较:

1
var array = [1, 1, '1', '1', null, null, undefined, undefined, new String('1'), new String('1'), /a/, /a/, NaN, NaN];
方法 结果 说明
双层 for 循环 [1, “1”, null, undefined, String, String, /a/, /a/, NaN, NaN] 对象和 NaN 不去重
Array.sort()加一行遍历冒泡 [/a/, /a/, “1”, 1, String, 1, String, NaN, NaN, null, undefined] 对象和 NaN 不去重 数字 1 也不去重
Array.filter()加 indexOf [1, “1”, null, undefined, String, String, /a/, /a/] 对象不去重 NaN 会被忽略掉
Object 键值对去重 [1, “1”, null, undefined, String, /a/, NaN] 全部去重
ES6中的Set去重 [1, “1”, null, undefined, String, String, /a/, /a/, NaN] 对象不去重 NaN 去重

内存考虑(去重复过程中,是想要空间复杂度最低吗?)

虽然说对于 V8 引擎,内存考虑已经显得不那么重要了,而且真的数据量很大的时候,一般去重在后台处理了。尽管如此,我们也不能放过任何一个可以证明自己优秀的,还是考虑一下,嘿嘿。

以上的所有数组去重方式,应该 Object 对象去重复的方式是时间复杂度是最低的,除了一次遍历时间复杂度为O(n) 后,查找到重复数据的时间复杂度是O(1),类似散列表,大家也可以使用 ES6 中的 Map 尝试实现一下。

但是对象去重复的空间复杂度是最高的,因为开辟了一个对象,其他的几种方式都没有开辟新的空间,从外表看来,更深入的源码有待探究,这里只是要说明大家在回答的时候也可以考虑到时间复杂度还有空间复杂度

另外补充一个误区,有的小伙伴会认为 Array.filter()indexOf 这种方式时间复杂度为 O(n) ,其实不是这样,我觉得也是O(n^2)。因为 indexOf 函数,源码其实它也是进行 for 循环遍历的。具体实现如下

1
2
3
4
5
6
7
8
9
String.prototype.indexOf = function(s) {
for (var i = 0; i < this.length - s.length; i++) {
if (this.charAt(i) === s.charAt(0) &&
this.substring(i, s.length) === s) {
return i;
}
}
return -1;
};

补充说明第三方库lodash

lodash 如何实现去重

简单说下 lodashuniq 方法的源码实现。

这个方法的行为和使用 Set 进行去重的结果一致。

当数组长度大于等于 200 时,会创建 Set并将 Set 转换为数组来进行去重(Set 不存在情况的实现不做分析)。当数组长度小于 200 时,会使用类似前面提到的 双重循环 的去重方案,另外还会做 NaN 的去重

常见javascript设计模式

模块设计模式

JS 模块化是使用最普遍的设计模式,用于保持特殊的代码块与其它组件之间互相独立。为支持结构良好的代码提供了松耦合。

对于熟悉面向对象的开发者来说,模块就是 JS 的 “类”。封装是“类”的众多优点之一,可以确保它本身的状态和行为不被其它的类访问到。模块设计模式有公有和私有两种访问级别(除此之外,还有比较少为人知的保护级别、特权级别)。

考虑到私有的作用域,模块应该是一个立即调用函数(IIFE) ,也就是说,它是一个保护其私有变量和方法的闭包。(然而,它返回的却不是一个函数,而是一个对象)。

这种模式我们可以通过即行函数 IIFE(immediately-invoked function expression)、闭包和函数作用域来模拟,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const myModule = (function () {
const privateVariable = "Hello World";

function privateMethod() {
console.log(privateVariable);
}
return {
publicMethod: function () {
privateMethod();
},
};
})();

myModule.publicMethod();

代码是即行函数的方法,通过立即执行函数,并把返回结果指向了 myModule 变量。由于闭包、返回的对象仍然可以访问定义在即行函数内的函数和变量,当然这些操作是在执行完即行函数产生实例之后了。我们可以看到,变量和方法被定义在即行函数内部,对于模块外部的作用域来说即达到了 private 的效果。

工厂模式

工厂模式是一种使用工厂函数来创建对象的设计模式,该模式不用指定被创建对象准确的类或者构造函数。这种模式通常用来去创建一些不用暴露实例化逻辑的对象。例如我们可以根据依赖对象中传递的不同实例化条件来动态生成所需要的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Car {
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || "brand new";
this.color = options.color || "white";
}
}
class Truck {
constructor(options) {
this.doors = options.doors || 4;
this.state = options.state || "used";
this.color = options.color || "black";
}
}
class VehicleFactory {
createVehicle(options) {
if (options.vehicleType === "car") {
return new Car(options);
} else if (options.vehicleType === "truck") {
return new Truck(options);
}
}
}

这里我分别定义了一个 Car 和一个 Truck 类,并给对象添加了默认值,这 2 个类分别用来创建各自的 car 和 truck 对象。然后我定义了一个 VehicleFactory 类,根据 options 对象中 vehicleType 属性来创建和返回新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const factory = new VehicleFactory();
const car = factory.createVehicle({
vehicleType: "car",
doors: 4,
color: "silver",
state: "Brand New",
});
const truck = factory.createVehicle({
vehicleType: "truck",
doors: 2,
color: "white",
state: "used",
});
// Prints Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Prints Truck {doors: 2, state: "used", color: "white"}
console.log(truck);

这里我用 VehicleFactory 类创建了一个工厂对象,然后分别指定两个 options 对象 vehicleType 属性的值为 car 和 truck,通过 factory.createVehicle 方法分别创建了 Car 和 Truck 对象。

观察者模式

很多时候,当应用的一部分改变了,另一部分也需要相应更新。在 AngularJs 里面,如果 $scope 被更新,就会触发一个事件去通知其他组件。结合观察这模式就是:如果一个对象改变了,它只要派发 broadcasts 事件通知依赖的对象它已经改变了则可。

观察者模式主要应用于对象之间一对多的依赖关系,当一个对象发生改变时,多个对该对象有依赖的其他对象也会跟着做出相应改变,这就非常适合用观察者模式来实现。使用观察者模式可以根据需要增加或删除对象,解决一对多对象间的耦合关系,使程序更易于扩展和维护。

又一个典型的例子就是 model-view-controller (MVC) 架构了;当 model 改变时, 更新相应的 view。这样做有一个好处,就是从 model 上解耦出 view 来减少依赖。

实现一个观察者模式至少要包含 2 个角色如下图 UML 图中所示:Subject 和 Observer 对象 。

img

下面我们使用 JavaScript 来实现上图的观察者模式。

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
var Subject = function () {
var observers = [];

return {
subscribeObserver: function (observer) {
observers.push(observer);
},
unsubscribeObserver: function (observer) {
var index = observers.indexOf(observer);
if (index > -1) {
observers.splice(index, 1);
}
},
notifyObserver: function (observer) {
var index = observers.indexOf(observer);
if (index > -1) {
observers[index].notify(index);
}
},
notifyAllObservers: function () {
for (var i = 0; i < observers.length; i++) {
observers[i].notify(i);
}
},
};
};

var Observer = function () {
return {
notify: function (index) {
console.log("Observer " + index + " is notified!");
},
};
};

上面的代码我们实现了 Subject 对象,在其内部声明了一个 observers 数组用来存储注册的 observer 对象。下面让我们来使用这两个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var subject = new Subject();

var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();

subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);

subject.notifyObserver(observer2); // Observer 2 is notified!

subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!

以上实现了一个简单的设计者模式,先使用 subjectsubscribeObserver(observer)注册以后要通知的观察者对象,当我们想通知注册好的观察者对象时,只需要使用 subject.notifyObserver(observer)即可

单例模式

单例模式只允许实例化一个对象,但是相同的对象,会用很多个实例。单例模式制约着客户端创建多个对象。第一个对象创建后,就返回实例本身。

JavaScript 语言本身就是支持单例模式的,不过我们一般并不称它为单例模式,我们通常叫它字面量对象,例如:

1
2
3
4
5
6
7
8
const user = {
name: "Peter",
age: 25,
job: "Teacher",
greet: function () {
console.log("Hello!");
},
};

JavaScript 中的每个对象在内存中都是唯一的,当我们调用 User 对象时,实质上也是返回的对象引用地址。

假如我们想要拷贝某个对象到另外一个变量,并且修改变量,该如何办呢?如下:

1
2
const user1 = user;
user1.name = "Mark";

我们会得到结果是两个对象的 name 都被修改了, 因为赋值的时候是引用赋值,而不是值赋值。所以内存中只有一份对象。请看:

1
2
3
4
5
6
// prints 'Mark'
console.log(user.name);
// prints 'Mark'
console.log(user1.name);
// prints true
console.log(user === user1);

单例模式可以通过构造函数来实现,看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var printer = (function () {
var printerInstance;

function create() {
return {
turnOn: function turnOn() {
console.log("working");
},
};
}

return {
getInstance: function () {
if (!printerInstance) {
printerInstance = create();
}
return printerInstance;
},
};
})();

printer.getInstance().turnOn(); // output: working

从上面代码我们可以看到 printer 模块提供了一个唯一外部可以访问的接口 getInstance,当第一次访问该接口时,我们先判断实例是否被创建,如果没有创建则使用 create()创建,如果已经创建则返回唯一的实例 printerInstance。

Promise实现两秒打印和函数柯理化

promise实现两秒打印一次成功或失败(随机数大于0.5成功,反之失败)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function setThreeSecond() {

setInterval(() => {
const promiseObj = new Promise((resolve, reject) => {
const s = Math.random()
if (s > 0.5) {
resolve('成功' + s)
} else {
reject('失败' + s)
}
})

promiseObj.then(res => {
console.log(res)
}, err => {
console.log(err)
})
}, 2000)
}

setThreeSecond()
实现add(1)(2)(3)(4)…累加器(柯里化)
1
2
3
4
5
6
7
8
9
10
11
12
function add(s){

function temp(a){
return add(s + a)
}

temp.toString = function(){
return s
}

return temp
}