# ES6 不完全手册(上)

# 前言

  此篇是阅读《ES6 标准入门》 (opens new window)的记录小册,保留了阅读当时的记忆和拓展,以便于后续查阅,分享出来,希望对你有用。关于ES6API更为详细的部分还是推荐参考《ES6标准入门》,只是文中相对会精简很多,同时也包括一些未提及的内容。

20156ECMAScript的第六个版本发布了,即通常所说的ES6(或者ES2015

# 语法提案

  一个新语法从提出到成为正式标准,需要经历 5个阶段 (opens new window),一般只要能进入Stage 2阶段,就可能会包括在以后的正式标准中。

  • Stage 0Strawman,稻草人阶段,只能由TC39成员或TC39构建者提出
  • Stage 1Proposal,提案阶段,只能由TC39成员发起,且方案中必须书面包含示例、API以及相关的语义和算法
  • Stage 2Draft,草案阶段,正式规范语言并且精确描述语法和语义
  • Stage 3Candidate,候选阶段,基本实现,等待用户反馈以改进
  • Stage 4Finished,定案阶段,必须通过Test262测试,准备纳入ECMAScript标准中

标准委员会,又称 TC39 (opens new window) 委员会,负责并管理着ECMAScript语言和标准化API

  比如 可选链 (opens new window)Optional Chaining)操作符?.,于2017年的Stage 0阶段,一直到2020年被正式地纳入了ES2020规范。

a?.b
a == null ? undefined : a.b

# Babel

  为了更好地理解一些API,或者了解ES6语法的ES5实现,可以安装 Babel (opens new window) 来转码。

  最为常用的就是命令行转码,最小安装babel/corebabel/clibabe/preset-env,其中babel/corebabel的核心依赖,babel/cli用于命令行转码,babel/preset-env为官方提供的预制器,能够根据配置自动添加插件和补丁来编译ES6+的代码。

npm install -D @babel/core @babel/cli @babel/preset-env

  添加.babelrc以引入babel/preset-env,并配置script命令,运行后将转换src中的ES6代码并输出到dist下。

// .babelrc
{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": []
}

// package.json
{
  ...
  "scripts": {
    "trans": "babel src -d dist"
  }
}

# 变量声明

  ;ES6声明变量的方式一共有六种,分别为varfunctionletconstclassimport,其中varfunctionES5的。

# var

  ;ES5中只有全局作用域和函数作用域。

  • 存在变量提升,并且会被提升至函数或全局作用域的顶部
  • 全局声明的变量会成为顶层对象的属性
  • 可以重复声明变量
console.log(foo) // undefined

if (true) {
  var foo = 2
  console.log(window.foo) // 2
}

注意if的判断条件无论是true还是false,都是不会影响变量提升的行为的

  稍不注意foo就声明为了全局变量,以上代码相当于。

var foo
console.log(foo) // undefined

if (true) {
  foo = 2
  console.log(window.foo) // 2
}

# let

  而let声明的变量相对就合理很多,并且引入了块级作用域。

  • 声明的变量仅在当前块级作用域内有效
  • 没有变量的提升,存在暂时性死区(TDZlet声明的变量之前,都是不可访问的)
  • 同一个作用域下不可重复声明同一个变量
  • 全局声明的变量不会挂在顶层对象上

  推荐一篇文章《两个月的时间理解 Let》 (opens new window),大致概括为js的变量包括创建(create)、初始化(initialize)和赋值(assign)三个阶段,其中。

  • 通常所说的var变量提升指的是提升了创建初始化两个阶段
  • let/const声明的变量将提升创建阶段,初始化阶段未被提升,因此严格来说没有变量提升也是对的
  • function声明的函数将提升创建初始化赋值三个阶段

  以下{}foo创建阶段被提升,而初始化阶段没有被提升,因此会提示在初始化之前无法访问foo,也即是TDZ形成的根本原因。

var foo = 1

{
  console.log(foo) // Uncaught ReferenceError: Cannot access 'foo' before initialization
  let foo = 2
}

  ;ES5typeof是一个绝对安全的操作,对于不存在的变量返回undefined,但是由于TDZ的形成,将不再成立。

typeof foo // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 1

# const

  ;const大体上与let一致,细微差异在于。

  • 一旦声明就要赋值,且以后不能再被修改
  • 声明复合类型时,可以修改其属性,但是不能将其指向另外的地址
const foo = {
  prop: 1
}

foo.prop = 2 // {prop: 2}
foo = {} // Uncaught TypeError: Assignment to constant variable

# for 循环

  ;for循环中存在两个作用域(循环条件和循环体),其中循环条件为父作用域,循环体为子作用域。以下输出3a,也印证了循环条件和循环体有着各自的作用域。

for (let i = 0; i < 3; i++) {
  let i = 'a'
  console.log(i)
}

  以下循环将连续输出55,未达到预期0 1 2 3 4的输出结果。

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  })
}

  以往ES5中是利用闭包来解决。

for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(() => {
      console.log(i)
    })
  })(i)
}

  而ES6中随着let的引入,问题得到很好的解决。

for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  })
}

  注意在每一轮循环时,JavaScript引擎都会为循环变量i单独生成一个新的变量,并且它会记住上一轮循环的值,并初始化本轮的值,以上代码相当于。

{
  let i = 0
  setTimeout(()=>{
    console.log(i)
  })
}
...
{
  let i = 4
  setTimeout(()=>{
    console.log(i)
  })
}

  ;babel转换let即是利用的闭包。

"use strict"

var _loop = function _loop(i) {
  setTimeout(function () {
    console.log(i)
  })
}

for (var i = 0; i < 5; i++) {
  _loop(i)
}

# 块级作用域下函数声明

  ;ES5在块级作用域中声明函数是非法的(浏览器不会报错),而ES6中是允许的。

function f() { console.log('outer') }

(function () {
  console.log(f)
  if (true) {
    console.log(f)
    function f() { console.log('inner') }
  }
  f()
}())

  以上代码在ES5中(IE10浏览器)运行,f将提升至函数顶部,相当于。

function f() { console.log('outer') }

(function () {
  function f() { console.log('inner') }
  console.log(f) // function f() { console.log('inner') }
  if (true) {
    console.log(f) // function f() { console.log('inner') }
  }
  f() // inner
}())

  但是对于ES6,相当于。

function f() { console.log('outer') }

(function () {
  console.log(f) // undefined
  if (true) {
    var f = function() { console.log('inner') }
    console.log(f) // function() { console.log('inner') }
  }
  f() // inner
}())

  因此对于ES6浏览器。

  • 允许在块级作用域声明变量
  • 类似var
  • 提升至当前所在块级作用域的顶部

严格模式下还有部分差异,详细可参考相关章节

  说了那么多,即由于环境差异较大,应当避免在块级作用域下声明函数,即使要使用,优先使用函数表达式,而不是声明语句。

{
  let f = function () { }
}

# 顶层对象

  ;ES2020引入了globalThis指向顶层对象,在任何环境下都是存在的。

  • 浏览器中,顶层对象是windowself也指向window
  • node环境下,顶层对象是global

  另外注意js文件在node环境下运行(例如node index.js)。

// index.js
var foo = 2
console.log(global.foo) // undefined

  打印undefined是必然的,原因是以上代码会被函数包裹,var声明的变量相当于只是函数中的局部变量,并不是全局变量。

function (exports, require, module, filename, dirname) {
  var foo = 2
  console.log(global.foo)
}

  如果进入node指令窗口(命令行运行node即可),运行刚才的代码,foo将是global上的属性。

# 解构

# 数组

  数组的元素是按次序排列的,解构时变量取值由它的位置决定。

  若被解构的元素是不可遍历的结构,将会报错。

const [foo] = 1 // Uncaught TypeError: 1 is not iterable
const [foo] = false // Uncaught TypeError: false is not iterable
const [foo] = NaN // Uncaught TypeError: NaN is not iterable
const [foo] = undefined // Uncaught TypeError: undefined is not iterable
const [foo] = null // Uncaught TypeError: null is not iterable
const [foo] = {} // Uncaught TypeError: {} is not iterable

  对象若要能遍历可以部署Symbol.iterator接口。

const foo = {
  [Symbol.iterator]() {
    return {
      next() {
        return {
          value: 1,
          done: false
        }
      }
    }
  }
}

const [first] = foo // 1

  另外只有当数组成员严格等于(===undefined时,默认值才会生效。

const [foo = 1] = [undefined] // 1
const [foo = 1] = [null] // null

  数组解构也可用于简化运算,以下实现了斐波那契数列。

for (var i = 0, x = 1, y = 1; i < 10; i++) {
  [x, y] = [x + y, x]
  console.log(x) // 2 3 5 8 13 21 34 55 89 144
}

  其它一些情况。

const [foo, [[bar], baz]] = [1, [[2], 3]] // 1 2 3
const [first, , ...rest] = [1, 2, 3, 4] // 1 [3, 4]
const [, second, ...rest] = 'hello' // e ["l", "l", "o"]

const foo = Object.create([1, 2, 3])
const [first] = foo // 1

  注意例3中由于字符串hello的原型上有Symbol.iterator,因此是可以遍历的。

  例4中创建的空对象foo的原型指向了数组[1, 2, 3],按理说空对象foo是不能遍历的,因为它没有部署遍历器接口,但是未获取到foo[Symbol.iterator]属性时,就会沿着原型链向上查找,即foo.__proto__[Symbol.iterator],也就是[1, 2, 3][Symbol.iterator],因此最终相当于是在解构数组[1, 2, 3]

你可以改写数组的Symbol.iterator方法,看看是否会执行

# 对象

  对象不同于数组的方式,变量与属性同名就能取到。另外对象解构也是当属性严格等于undefined时,默认值才生效。

  指定变量名。

const { foo: baz } = { foo: 1 }
console.log(baz) // 1

  对象解构也能获取到原型链上的值。

const foo = {}
Object.setPrototypeOf(foo, { baz: 'baz' })
const { baz } = foo // baz

  解构对象上的复合类型,以下均是浅拷贝。

const model = { value: 1 }
const list = [1, 2, 3]
const { prop: { model: foo, list: bar } } = { prop: { model, list } }
console.log(foo === model, list === bar) // true true

  其它一些情况。

const { foo, ...rest } = { foo: 1, bar: 2, baz: 3 } // 1 {bar: 2, baz: 3}
const { foo = 1, bar = 2, baz } = { foo: null } // null 2 undefined
const { foo, foo: { bar } } = { foo: { bar: 3 } } // {bar: 3} 3
const { 0: first, length } = [1, 2, 3] // 1 3

# 其它类型

  对于数值或布尔值,会先转为对象再解构。例如数值123,会调用Number将其包装为数值对象,即new Number(123)

const { toString: fn } = 123
fn === Number.prototype.toString // true

const { toString: fn } = true
fn === Boolean.prototype.toString // true

  对于字符串,对象方式的解构时会调用String将其包装为字符串对象,也就是类数组对象。

// new String('hello') {0: "h", 1: "e", 2: "l", 3: "l", 4: "o", length: 5}
const { 1: second, ...rest } = 'hello' // e {0: "h", 2: "l", 3: "l", 4: "o"}
const { length } = 'hello' // 5

# 字符串

# JSON

  ;你不知道的 JSON.stringify 特性

# 模板字符串

  若插值内是一个对象,将默认调用它的toString方法。

const msg = {
  toString() {
    return 'world'
  }
}
console.log(`hello ${msg}`) // hello world

const l = [1, 2, 3]
console.log(`${l}`) // 1,2,3

  模板字符串中,若插值有n个,则调用它的标签函数的参数就有n + 1个,除第一个参数外,其余参数均为各个插值表达式的计算结果,而第一个参数为模板字符串切割掉各个插值所剩下的字符串组成的数组。

另外第一个参数上还有一个raw属性,也是数组,它的每一项对应第一个参数中的每一项,然后在它们的斜杠前面再加一个斜杠,使其转义效果都失效。

  以下模板字符串中插值有3个,因此标签函数tag的参数有4个,除第一个参数外,标签函数剩余的参数分别为1 2 3,第一个参数为数组["", " + ", " = ", ""],它的raw属性为["", " \x2b ", " = ", ""],而raw中虽然显示的是\x2b,但是实际上返回的是\\x2b

const a = 1, b = 2
const tag = (array, ...args) => { // ["", " + ", " = ", "", raw: ["", " \x2b ", " = ", ""]] [1, 2, 3]
  console.log(array.raw[1] === ' \\x2b ') // true
  return array.reduce((prev, next, i) => {
    return prev + args[i - 1] + next
  })
}

tag`${a} \x2b ${b} = ${a + b}` // 1 + 2 = 3

  唯一一个内置的标签函数是String.raw(),它的作用很简单,作为标签函数使用时,在其斜杠前面再加一个斜杠,作为普通函数时,模拟插值拼接参数。比如例2中模拟的是h${0}e${1}l${2}lo,例3模拟的是foo${1 + 2}bar${'e' + 'f'}baz,而例4则是将字符串打散再还原。

String.raw`\\n` === '\\\\n' // true
String.raw({ raw: 'hello' }, 0, 1, 2) // h0e1l2lo
String.raw({ raw: ['foo', 'bar', 'baz'] }, 1 + 2, 'e' + 'f') // foo3barefbaz

const a = 1, b = 2
const tag = (raw, ...args) => { // ["", " + ", " = ", ""] [1, 2, 3]
  return String.raw({ raw }, ...args)
}
tag`${a} \x2b ${b} = ${a + b}` // 1 + 2 = 3

# includes / startWith / endsWith

  ;includes (opens new window) 用于判断一个字符串是否在另一个字符串中,返回布尔值,注意第二个为可选参数,表示开始搜索的位置,默认为0

if (!String.prototype.includes) {
  String.prototype.includes = function (searchString, position) {
    return this.indexOf(searchString, position) !== -1
  }
}

'hello world'.includes('llo', 1) // true

  ;startsWith (opens new window) 用于判断一个字符串是否在另一个字符串的头部,它的第二个参数与includes一致。

if (!String.prototype.startsWith) {
  String.prototype.startsWith = function (searchString, position) {
    return this.slice(position, position + searchString.length) === searchString
  }
}

'hello world'.startsWith('llo', 2) // true

  ;endsWith (opens new window) 用于判断一个字符串是否在另一个字符串的尾部,注意它的第二个参数表示针对前n个字符。

if (!String.prototype.endsWith) {
  String.prototype.endsWith = function (searchString, length) {
    return this.slice(length - searchString.length, length) === searchString
  }
}

'hello world'.endsWith('llo', 5) // true

# repeat

  ;repeat (opens new window) 表示将字符串重复n次,返回一个新的字符串。

if (!String.prototype.repeat) {
  String.prototype.repeat = function (count) {
    for (var result = '', weight = this, n = count; n > 0;) {
      if (n & 1) {
        result += weight
      }

      n = n >>> 1
      weight += weight
    }

    return result
  }
}

'ab'.repeat(5) // ababababab

  注意ES5的实现中,weight为相对应二进制位的权值,例如二进制...0111中,第二、三、四位权值分别为ababababababab。另外n & 1表示取n的最后一个二进制位,若为1则加对应权值,为0不加。n >>> 1表示将数n的二进制位右移一位,利用for循环,从最后一位开始依次获取每一个二进制位的值。

  因此当n5时(二进制...0101),会经过三次循环,第一次n & 1获取最后一个二进制位的值,为1(对应权值为ab),拼接后resultab,然后n右移一位(...0010),权值为abab。第二次n & 1获取的二进制位为0,不拼接,n右移一位(...0001),权值为abababab。第三次n & 1获取的二进制位为1(对应权值abababab),拼接后resultababababab,然后n右移一位(...000),此时n > 0不再满足,退出循环。

# padStart / padEnd

  ;padStart (opens new window)padEnd (opens new window) 都是ES2017引入的,用于补全字符串长度,padStart用于补全头部,padEnd用于补全尾部。

  注意以下repeat为字符串重复函数,另外padEndpadStartES5实现类似。

if (!String.prototype.padStart) {
  String.prototype.padStart = function (targetLength, padString) {
    padString = padString || ''

    var fillLen = targetLength - this.length
    padString = padString.repeat(Math.ceil(fillLen / padString.length))
    return padString.slice(0, fillLen) + this
  }
}

'abc'.padStart(10, "foo") // "foofoofabc"
'abc'.padEnd(10, "foo") // "abcfoofoof"

# trimStart / trimEnd

  ;trimStart (opens new window)trimEnd (opens new window) 都是ES2019引入的,用于去除字符串头尾的空白符,trimStart(别名trimLeft)用于去除头部的空白符,trimEnd(别名trimRight)用于去除尾部的空白符。

  注意此处的空白符包括空格、换行符\n、制表符\t等,另外\s表示匹配任何空白字符。

if (!String.prototype.trimStart) {
  String.prototype.trimStart = function () {
    return this.replace(/^\s+/, '')
  }
}

if (!String.prototype.trimEnd) {
  String.prototype.trimEnd = function () {
    return this.replace(/\s+$/, '')
  }
}

const s = '  foo  '
s.trim() // 'foo'
s.trimStart() // 'foo  '
s.trimEnd() // '  foo'

# 正则

# 表达式

  ;JavaScript 正则表达式

# 具名组匹配

  ;ES6提取组匹配结果。

const reg = /(\d{4})-(\d{2})-(\d{2})/
const [, y, m, d] = reg.exec('2020-12-07') // 2020 12 07
const format = '2020-12-07'.replace(reg, '$1/$2/$3') // 2020/12/07

  缺点也非常明显,无法直观了解每个组的匹配含义。ES2018引入了具名组匹配,便于描述组的匹配含义。

const reg = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/
const { groups: { y, m, d } } = reg.exec('2020-12-07') // 2020 12 07
const format = '2020-12-07'.replace(reg, '$<y>/$<m>/$<d>') // 2020/12/07

# 数值

# 进制

  在ES5中整数可由十六进制(0x0X)、八进制(0)和十进制表示。ES6中新增了八进制(0o0O)和二进制(0b0B)表示。

注意在严格模式下,ES5的八进制(0)表示不允许使用

  另外 Number (opens new window)Number.prototype.toString (opens new window)parsetInt (opens new window) 都是可用于进制转换的。其中Number可转为十进制数值,Number.prototype.toString可转为固定进制的字符串,parseInt可转为十进制数值。

Number('0b11') // 3
Number(0x11) // 17
(0o11).toString(10) // "9"
parseInt(11, 3) // 4
parseInt('1f', 16) // 31

# Number.isFinite

  ;Number.isFinite (opens new window) 用于检查一个数值是否有限,注意NaN也是数值,typeof NaNnumber

  另外只要参数不是数值类型,均返回false

Number.isFinite(10) // true
Number.isFinite(NaN) // false
Number.isFinite(-Infinity) // false
Number.isFinite('10') // false

  ;ES5兼容。

if (!Number.isFinite) {
  Number.isFinite = function (value) {
    return typeof value === 'number' && isFinite(value)
  }
}

  全局方法 isFinite (opens new window) 存在隐式类型转换,会将参数转换为数值,转换后若值为NaN或者Infinity则返回false,否则为true

isFinite('10') // true

const object = {
  valueOf() {
    return 10
  }
}
isFinite(object) // true

# Number.isNaN

  ;Number.isNaN (opens new window) 用于检查一个数值是否为NaN。若参数不是数值,也是均返回false

Number.isNaN(NaN) // true
Number.isNaN(Infinity) // false
Number.isNaN(10) // false
Number.isNaN('10') // false

  ;ES5兼容。

if (!Number.isNaN) {
  Number.isNaN = function (value) {
    return typeof value === 'number' && isNaN(value)
  }
}

  全局方法 isNaN (opens new window) 也存在隐式类型转换,将参数转换为数值后再判断是否为NaN

isNaN('NaN') // true

const object = {
  valueOf() {
    return NaN
  }
}
isNaN(object) // true

# Number.isInteger

  ;Number.isInteger (opens new window) 用于判断一个数值是否为整数,若参数为非数值,则返回false

Number.isInteger(3) // true
Number.isInteger(3.1) // false
Number.isInteger(Infinity) // false
Number.isInteger(NaN) // false
Number.isInteger('3') // false

  ;ES5兼容,对比Number.isFinite的兼容方式,Number.isInteger要求参数先要是有限的数值。

if (!Number.isInteger) {
  Number.isInteger = function (value) {
    return typeof value === "number" &&
      isFinite(value) &&
      Math.floor(value) === value
  }
}

  注意由于JavaScript内部对于Number类型采用IEEE 754双精度浮点数存储,某些数值可能产生精度损失,因此若精度要求较高还是不要使用。

Number.isInteger(3.0000000000000002) // true

# 浮点数

  ;JavaScript 浮点数陷阱

  ;JavaScript 浮点数取整

# 函数

# length

  ;length (opens new window) 为函数对象的属性值,用于指明函数的形参个数,返回第一个具有默认值之前的参数个数

  ;length即预期传入的参数个数,若参数指定了默认值,预期传入的参数也就不包括此参数了,另外剩余参数也不会计入length中。

(function (a, b, c) { }).length // 3
(function (a, b = 2, c) { }).length // 1
(function (a, b, ...rest) { }).length // 2

与之对应的,arguments.length (opens new window) 将返回实参个数

# 作用域

  ;关于 ES6 参数默认值形成的第三作用域问题

# 剩余参数

  ;剩余参数 (opens new window) 用于获取函数的多余参数。

function f(first, second, ...rest) {
  console.log(rest) // [3, 4]
}
f(1, 2, 3, 4)

  ;babel转换为es5代码。

"use strict"

function f(first, second) {
  for (var _len = arguments.length, rest = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
    rest[_key - 2] = arguments[_key]
  }
  console.log(rest)
}
f(1, 2, 3, 4)

# 严格模式

  ;JavaScript 严格模式差异性对比

# Function.name

  ;Function.name (opens new window) 返回函数名称,非标准属性,在ES6被纳入标准,以下仅为ES6的结果。

  ;function声明的函数。

function foo() { }
foo.name // foo

(function () { }).name // ""

  变量声明的方式。

var f = function () { }
f.name // f

  ;function与变量声明两种方式时,返回函数原名称。

var fn = function func() { }
fn.name // func

  ;bound前缀。

var fn = function func() { }
fn.bind().name // bound func

  ;anonymous匿名函数。

var t = new Function('x', 'y', 'return x + y')
t.name // anonymous
t.bind().name // bound anonymous

注意IE浏览器不支持Function.name属性

# 箭头函数

  ;JavaScript 箭头函数

# 尾递归

  ;关于取消 ES6 函数尾递归的相关探究

# 数组

# 扩展运算符

  扩展运算符(... (opens new window))用于将数组转为逗号分割的参数序列。

f(...[1, 2, 3]) 

// 转换为
f(1, 2, 3)

// 与 apply 类似
f.apply(null, [1, 2, 3])

  注意只有函数调用时,...才能放在括号中,否则解析阶段就会报错。

console.log((...[1, 2])) // Uncaught SyntaxError: Unexpected token '...'

  数组参数传给构造函数的场景。

var args = [1, 2]
function F(x, y) {
  this.x = x
  this.y = y
}

// ES6
new F(...args) // F {x: 1, y: 2}

// ES5 方式一
function _F(args) {
  var object = Object.create(F.prototype)
  F.apply(object, args)

  return object
}
new _F(args) // F {x: 1, y: 2}

// ES5 方式二
var _F = (function (constructor) {
  function F(args) {
    constructor.apply(this, args)
  }

  F.prototype = constructor.prototype

  return F
})(F)
new _F(args) // F {x: 1, y: 2}

  ;ES6的方式最为简单清晰。对于ES5的第一种方式,运用了若构造函数有返回值且为对象,则返回此对象。ES5的第二种方式,自执行函数返回了内部函数Fnew _F()相当于new F()F为内部函数),而构造函数无返回值时,将返回this,因此实际返回的是内部函数F的实例。

  扩展运算符除了可以复制数组(浅拷贝)、合并数组之外,还能正确识别Unicode字符。

'𠮷'.length // 2
[...'𠮷'].length // 1

  也可以解构部署了Symbol.iterator接口的对象。

Number.prototype[Symbol.iterator] = function () {
  var length = this
  var index = 0

  return {
    next() {
      return {
        done: index > length,
        value: index++,
      }
    },
  }
}

[...5] // [0, 1, 2, 3, 4, 5]

# Array.from

  ;Array.from (opens new window) 用于将类似数组和可遍历对象转化为数组。

var arrayLike = {
  0: 'foo',
  1: 'bar',
  length: 2,
}

// ES5
[].slice.call(arrayLike) // ["foo", "bar"]

// ES6
Array.from(arrayLike) // ["foo", "bar"]

  以下为Array.from的简单实现,其中mapFn用来对新数组的每个元素进行处理,作用类似数组的map方法。

Array.from = function (arrayLike, mapFn) {
  var iterator, result, step, length
  var index = 0
  var O = Object(arrayLike)
  var iteratorMethod = O[Symbol.iterator]

  if (iteratorMethod) {
    iterator = iteratorMethod.call(O)

    for (result = []; !(step = iterator.next()).done; index++) {
      result[index] = step.value
    }
  } else {
    result = Array.prototype.slice.call(O)
  }

  return mapFn ? result.map(mapFn) : result
}

# Array.of

  ;Array.of (opens new window) 用于将参数转换为数组。

Array.of(1, 2, 3) // [1, 2, 3]

  ;ES5兼容。

if (!Array.of) {
  Array.of = function () {
    return Array.prototype.slice.call(arguments)
  }
}

# copyWithin

  ;ES6 copyWithin

# find

  ;find (opens new window) 用于返回数组中第一个符合条件的成员。

[1, 4, -5, 10].find(n => n < 0) // -5

  ;ES5兼容。

if (!Array.prototype.find) {
  Array.prototype.find = function (callbackfn, thisArg) {
    for (var i = 0; i < this.length; i++) {
      if (callbackfn.call(thisArg, this[i], i, this)) {
        return this[i]
      }
    }
  }
}

# findIndex

  ;findIndex (opens new window) 用于返回数组中第一个符合条件的成员的索引。

[1, 4, -5, 10].findIndex(n => n < 0) // 2

  ;ES5兼容。

if (!Array.prototype.findIndex) {
  Array.prototype.findIndex = function (callbackfn, thisArg) {
    for (var i = 0; i < this.length; i++) {
      if (callbackfn.call(thisArg, this[i], i, this)) {
        return i
      }
    }

    return -1
  }
}

# fill

  ;fill (opens new window) 用于填充数组。

[1, 2, 3].fill(4, 1, 2) // [1, 4, 3]

  ;ES5 兼容。

function toAbsoluteIndex(target, len) {
  return target < 0 ? len + target : Math.min(target, len)
}

if (Array.prototype.fill) {
  Array.prototype.fill = function (value, start, end) {
    var len = this.length
    start = toAbsoluteIndex(start || 0, len)
    end = end === undefined ? len : toAbsoluteIndex(end, len)

    while (start < end) {
      this[start++] = value
    }

    return this
  }
}

# keys

  ;keys (opens new window) 返回一个遍历器对象,可用for...of遍历或者next调用,遍历结果为键名。

for (const result of ['a', 'b', 'c'].keys()) {
  console.log(result)
  // 0
  // 1
  // 2
}

  ;keysGenerator实现。

Array.prototype.keys = function* () {
  for (var i = 0; i < this.length; i++) {
    yield i
  }
}

# values

  ;values (opens new window) 返回一个遍历器对象,遍历结果为键值。

const values = ['a', 'b', 'c'].values() // Array Iterator {}

values.next() // {value: "a", done: false}
values.next() // {value: "b", done: false}

  ;valuesGenerator实现。

Array.prototype.values = function* () {
  for (var i = 0; i < this.length; i++) {
    yield this[i]
  }
}

  实际上原型上values[Symbol.iterator]是等价的。

Array.prototype.values === Array.prototype[Symbol.iterator] // true

# entries

  ;entries (opens new window) 返回一个遍历器对象,遍历结果为键值对。

for (const result of ['a', 'b', 'c'].entries()) {
  console.log(result)
  // [0, "a"]
  // [1, "b"]
  // [2, "c"]
}

  ;entriesGenerator实现。

Array.prototype.entries = function* () {
  for (var i = 0; i < this.length; i++) {
    yield [i, this[i]]
  }
}

# includes

  ;includes (opens new window) 用于判断数组是否包括指定的值,含NaN。其中第二个参数fromIndex用于指定开始查找的位置。

[1, 2, 3].includes(1) // true
[1, 2, NaN].includes(NaN) // true

[1, 2, 3].includes(1, 1) // false

  注意fromIndex若大于数组长度,将返回false。若小于0将从fromIndex + length位置开始,若还是小于0,则从0开始。

[1, 2, 3].includes(1, 5) // false

[1, 2, 3].includes(1, -2) // false
[1, 2, 3].includes(1, -5) // true

  ;ES5兼容。

function toAbsoluteIndex(target, len) {
  return target < 0 ? len + target : Math.min(target, len)
}

function isNaNumber(value) {
  return typeof value === 'number' && isNaN(value)
}

function isEqual(x, y) {
  return x === y || (isNaNumber(x) && isNaNumber(y))
}

if (!Array.prototype.includes) {
  Array.prototype.includes = function (el, fromIndex) {
    var len = this.length
    fromIndex = fromIndex || 0
    var i = fromIndex + len < 0 ? 0 : toAbsoluteIndex(fromIndex, len)

    while (i < len) {
      if (isEqual(this[i], el)) {
        return true
      }

      i++
    }

    return false
  }
}

# flat

  ;ES6 flat 与数组扁平化

# flatMap

  ;flatMap (opens new window) 用于对数组进行map,然后进行一层扁平化,执行效率相对高一些。

Array.prototype.flatMap = function (callbackFn, thisArg) {
  var arr = this.filter(() => true)

  return arr.map(callbackFn.bind(thisArg)).flat(1)
}

arr.flatMap(x => [[x * 2]]) // [[2], [4], [6], [8]]

  注意map虽然会在循环中跳过empty空位,但是却仍然将保留空位,利用filter可以跳过且不保留空位。

# at

  ;at (opens new window) 用于返回数组对应索引值的成员,可以为正数或者负数。

Array.prototype.at = function (index) {
  return this[index < 0 ? this.length + index : index]
}

var arr = [2, 3, 4]
arr.at(1) // 3
arr.at(-1) // 4

# 空位

  数组空位表示数组某一位置没有任何值。

[, ,] // [empty, empty]
new Array(3) // [empty, empty, empty]

  可以理解为开辟了内存空间,但是空间中没有保存任何的变量,访问时只有返回undefined

var arr = [5, , 7] // [5, empty, 7]
arr[1] === undefined // true

# 跳过空位

  ;ES5中的很多遍历方法都将跳过空位,例如forEachfilterreduceeverysomefor...in也是。

[5, , 7].forEach(function (v, i) {
  console.log(i, v)
  // 0 5
  // 2 7
}

for (var i in [5, , 7]) {
  console.log(i)
  // 0
  // 2
}

  比较特殊的,map也将跳过空位,但是结果会保留空位值。

[5, , 7].map(function (v, i) {
  console.log(i, v)
  // 0 5
  // 2 7
  return v
}) // [5, empty, 7]

# 不跳过空位

  ;ES6中的很多遍历方法则不会跳过空位,例如findfindIndexfor...of也是。

for (var v of [5, , 7]) {
  console.log(v)
  // 5
  // undefined
  // 7
}

[5, , 7].find((v, i) => {
  console.log(i, v)
  // 0 5
  // 1 undefined
  // 2 7
  return false
})

# 空位拷贝

  刚才也说了,空位是有内存空间的,但是没有存储变量。所以数组的拼接(concat)、转接(copyWithin)、翻转(reverse)、成员删除、添加或修改(splice)、取出或弹出(slicepop)等,空位都将依然存在。

[5, , 7].copyWithin(0, 1, 2) // [empty, empty, 7]
[, , 1].reverse() // [1, empty, empty]

# 访问

  访问数组的方法,例如valuesArray.from等,则明确返回undefined

[...[5, , 7].values()] // [5, undefined, 7]

  注意数组的jointoString方法会将undefined或者null转换为空字符串。而empty空位相当于是undefined,因此空位也会被转换为空字符串。

[5, null, undefined].toString() // "5,,"
[5, null, undefined, ,].join('#') // "5###"

  还有一个sort较为特殊,空位会被置后。

[9, , 5, 7].sort((x, y) => x - y) // [5, 7, 9, empty]
[9, , 5, 7].sort((x, y) => y - x) // [9, 7, 5, empty]

# 小结

  • ES5的遍历方法,例如forEachreducesome等,将跳过空位。而map虽然会跳过,但是结果中会保留空位
  • ES6的遍历方法,findfindIndex等,不会跳过空位
  • 空位只是内存空间没有存储变量,故拼接、翻转等,空位依然存在
  • 明确访问空位将返回undefined。在jointoString时将被转换为空字符串。sort时会将空位置后

# sort

  ;JavaScript 中常见的排序类型

# 对象

# 简写

  对象方法。

const o = {
  method: function () {
    return 'hello'
  }
}

console.log(o.method.prototype) // {constructor: ƒ}
console.log(new o.method()) // method {}

  以下为简写方式,注意简写后方法原型丢失,也不能作为构造函数了。根本原因在于简写后的方法,浏览器不再赋予[[Construct]]内部槽,因此不能作为构造函数,更多可参考箭头函数章节。

const o = {
  method() {
    return 'hello'
  }
}

console.log(o.method.prototype) // understand
console.log(new o.method()) // Uncaught TypeError: o.method is not a constructor

# 描述符

  ;JavaScript 属性描述符

# Object.is

  ;Object.is (opens new window) 用于判断两个值是否严格相等,与===运算符相比较,Object.is(NaN, NaN)返回true,但是Object(-0, 0)返回false

  ;ES5兼容。

if (!Object.is) {
  Object.is = function (x, y) {
    if (x === y) {
      return x !== 0 || 1 / x === 1 / y
    } else {
      return x !== x && y !== y
    }
  }
}

  其中在x === y时,考虑xy是否为0,若不为0,则返回true。若为0,则运用1 / x === 1 / y判断两者符号是否相同。

  注意一个变量若满足v !== v,即为NaN。因此x !== x && y !== y用于判断xy是否都是NaN

# Object.assign

  ;Object.assign (opens new window) 用于将源对象的可枚举属性,浅复制到目标对象,并返回目标对象。

  也可用来合并数组。

const foo = [1, 2, 3, 4]
const bar = [5, 6, 7]

Object.assign(foo, bar)
foo // [5, 6, 7, 4]

  源对象属性若为取值函数,将求值后再复制。

const x = 1, y = 2
const foo = {}
const bar = {
  get z() { return x + y }
}

Object.assign(foo, bar)
foo // {z: 3}

  基本类型会被包装为对象,再复制到目标对象,注意仅字符串会被合入对象。

Object.assign({}, 'hello', undefined, null, 10, false) // {0: 'h', 1: 'e', 2: 'l', 3: 'l', 4: 'o'}
Object.assign({}, new String('hello')) // {0: 'h', 1: 'e', 2: 'l', 3: 'l', 4: 'o'}

  ;ES5兼容,其中objectKeys用于获取对象原有(非继承)的属性。

function objectKeys(object) {
  var keys = []

  for (var key in object) {
    if (Object.hasOwnProperty.call(object, key)) {
      keys.push(key)
    }
  }

  return keys
}

if (!Object.assign) {
  Object.assign = function (target) {
    var T = Object(target)

    for (var i = 1; i < arguments.length; i++) {
      var S = Object(arguments[i])
      var keys = objectKeys(S)
      var key

      for (var j = 0; j < keys.length; j++) {
        key = keys[j]
        T[key] = S[key]
      }
    }

    return T
  }
}

# Object.getOwnPropertyDescriptors

  ;Object.getOwnPropertyDescriptors (opens new window) 用于返回对象自身的所有属性的描述符。

var object = {
  foo: 1,
  get bar() {},
}

Object.getOwnPropertyDescriptors(object)
// {
//   bar: {
//     configurable: true,
//     enumerable: true,
//     get: f bar(),
//     set: undefined,
//   },
//   foo: {
//     configurable: true,
//     enumerable: true,
//     value: 1,
//     writable: true,
//   },
// }

  兼容IE8

function ownKeys(object) {
  var keys = []

  for (var key in object) {
    if (Object.hasOwnProperty.call(object, key)) {
      keys.push(key)
    }
  }

  return keys
}

if (!Object.getOwnPropertyDescriptors) {
  Object.getOwnPropertyDescriptors = function (object) {
    var key, result = {}, keys = ownKeys(object)

    for (var i = 0; i < keys.length; i++) {
      key = keys[i]

      result[key] = Object.getOwnPropertyDescriptor(object, key)
    }

    return result
  }
}

# Object.setPrototypeOf

  ;Object.setPrototypeOf (opens new window) 用于指定对象的原型。

Object.getPrototypeOf (opens new window) 用于获取对象原型

  兼容IE

function setPrototypeOf(object, prototype) {
  object.__proto___ = prototype

  return object
}

function mixinProperties(object, prototype) {
  for (var prop in prototype) {
    if (!Object.hasOwnProperty.call(object, prop)) {
      object[prop] = prototype[prop]
    }
  }

  return object
}

if (!Object.setPrototypeOf) {
  Object.setPrototypeOf = function (object, prototype) {
    return '__proto__' in {} ? setPrototypeOf(object, prototype) : mixinProperties(object, prototype)
  }
}

# Object.keys

  ;Object.keys (opens new window) 用于返回对象的自身可枚举属性组成的数组。

  ;ES5兼容。

if (!Object.keys) {
  Object.keys = function (object) {
    var result = []

    for (var prop in object) {
      if (Object.hasOwnProperty.call(object, prop)) {
        result.push(prop)
      }
    }

    return result
  }
}

# Object.values

  ;Object.values (opens new window) 用于返回对象的自身可枚举属性值组成的数组。

  ;ES5兼容。

if (!Object.values) {
  Object.values = function (object) {
    var result = []

    for (var prop in object) {
      if (Object.hasOwnProperty.call(object, prop)) {
        result.push(object[prop])
      }
    }

    return result
  }
}

# Object.entries

  ;Object.entries (opens new window) 用于返回对象的自身可枚举属性键值对组成的数组。

  ;ES5兼容。

if (!Object.entries) {
  Object.entries = function (object) {
    var result = []

    for (var prop in object) {
      if (Object.hasOwnProperty.call(object, prop)) {
        result.push([prop, object[prop]])
      }
    }

    return result
  }
}

# Object.formEntries

  ;Object.formEntries (opens new window) 用于将键值对转换为对象。

var map = new Map()
map.set('foo', 1)
map.set({}, 'Object')

Object.fromEntries(map) // {foo: 1, [object Object]: 'Object'}

Object.fromEntries(new URLSearchParams('foo=1&bar=2'))
// {foo: '1', bar: '2'}

  替代版本。

Object.fromEntries = function (iterable) {
  var result = {}

  for (const [key, value] of iterable) {
    result[key] = value
  }

  return result
}

# Object.hasOwn

  ;Object.hasOwn (opens new window) 用于判断属性是否为对象自身的,旨在替代Object.prototype.hasOwnProperty

  以下为判断对象是否含有某个属性。

var object = { foo: 1 }

object.hasOwnProperty('foo') // true

  但是对于原型为null的对象就会报错,原因在于沿着原型链是查找不到hasOwnProperty方法的。

var object = Object.create(null)

object.hasOwnProperty('foo') // Uncaught TypeError: object.hasOwnProperty is not a function

  所以最常见的判断方式为。

var object = Object.create(null)

Object.prototype.hasOwnProperty.call(object, 'foo') // false

  写法上不免复杂,也不容易理解,而以Object.hasOwn相对简洁很多。

var object = Object.create(null)

Object.hasOwn(object, 'foo') // false

  兼容ES5

if (!Object.hasOwn) {
  Object.hasOwn = function (object, prop) {
    return Object.prototype.hasOwnProperty.call(object, prop)
  }
}

下一篇

# 🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHub (opens new window) / Gitee (opens new window)GitHub Pages (opens new window)掘金 (opens new window)CSDN (opens new window) 同步更新,欢迎关注😉~

最后更新时间: 8/12/2022, 10:20:59 AM