understanding es6


title: 【翻译】理解 ES6(Understanding ECMAScript 6)
date: <2019-06-16 Sun>
updated: <2019-06-16 Sun>
comments: true
tags:javascript

  • javascript
  • es6

categories: ecmascript
layout: post
permalink:
top: 10
copyright: truehtml


{% note info %}
原书:leanpub.com/understandi…java

该书为我的为学习而翻译的,文中几乎全部代码都是来自原书或在原书代码基础上修改而来。node

该原文地址:blog.gcl666.com/2019/06/23/… {% endnote %}git

简介

JavaScript 核心特性在 ECMA-262 标准中被定义,也叫作 ECMAScript ,咱们所熟知的在浏览器端和 Node.js 其实是 ECMAScript 的一个超集。程序员

ES6 演变之路

1999 发布 v3

1999.TC39 年发布了 ECMA-262 第三版。es6

直到 2007 以前都没有任何变化。github

2007 发布 v4, v3.1

2007 年发布了第四版,包含如下特性:web

  • 新语法(new syntax)
  • 模块(modules)
  • 类概念(classes)
  • 类继承概念(classical inheritance)
  • 对象私有属性(private object members)
  • 更多类型
  • 其余

因为第四版涉及的内容太多,所以形成分歧,部分红员由此建立了正则表达式

3.1 版本,只包含少部分的语法变化,聚焦在:

  • 属性
  • 原生 JSON 支持
  • 已有对象增长方法

可是两拨人在 v4 和 v3.1 版本之间并无达成共识,致使最后不了了之。

2008 JavaScript 创始人决定

Brendan Eich 决定将着力于 v3.1 版本。

最后 v3.1 做为 ECMA-262 的第五个版本被标准化,即: ECMASCript 5

2015 年发布 ECMAScript 6 也叫 ECMAScript 2015

即本书要讲的内容(ES6)。

块级绑定(var, let, const)

Var 声明和提高

使用 var 来声明变量时,在一个做用域内,不管它在哪里声明的,都会被升到到该做用域的顶部。

好比:

function getValue(condition) {
  // 好比: var value; // undefined

  if (condition) {
    // 虽然在这里声明的,其实会被提高到函数顶端
    var value = 'blue'

    // code

    return value
  } else {
    // 这里依旧能够访问变量 `value` 只不过它的值是 `undefined`
    return null
  }
}

console.log(getValue(false)) // 'null'
复制代码

上面的 getValue 至关于下面的变量声明版本(提高以后):

function getValue(condition) {
  var value; // undefined

  if (condition) {
    value = 'blue'

    // code

    return value
  } else {
    return null
  }
}

console.log(getValue(false)) // 'null'

复制代码

+RESULTS:

null
复制代码

块级声明 let/const 声明

块级做用域,如:函数,*{ … }* 大括号,等等都属于块级做用域,在该做用域下使用 let 声明的变量只在

该做用域下可访问。

声明提高问题

let 声明不会被提高,可是也有另外一种说法是 let 会提高,而且在若是在提高处到赋值的中间范围内使用了该变量,

会使该区域成为一块临时死区(TDZ)。

在声明以前使用 let 变量:

VM88:4 Uncaught ReferenceError: Cannot access 'value' before initialization

function getValue(cond) {

  if (cond) {
    console.log(value)
    let value = 'blue'

    // code

    return value
  } else {
    // value 在该做用域不存在

    return null
  }

  // value 在该做用域不存在
}

getValue(true)

复制代码

不能重复声明

使用 var 的时候是能够重复声明的:

var count = 39; var count;

这样是不会有问题的,只不过它的声明只会被记录一次而已,即只会记录 var count = 39; 这里声明,可是不会出现异常。

若是使用 let 就不同了,若是出现重复声明则会异常:

var count = 39;let count;

异常结果:*SyntaxError: Identifier 'count' has already been declared*

二者差异

let 声明的值可变,const 声明的是个常量,值是不能发生改变的。

let name = 'xxx';

name = 'yyy'; // ok

const age = 100;

age = 88; // error
复制代码

临时死区(TDZ)

使用 let/const 声明的变量,任什么时候候试图在其声明以前使用变量都会抛出异常。

即便是在声明以前使用 typeof 也会出现引用异常(ReferenceError)。

if (true) {
  console.log(typeof value)
  let value = 'blue'
}

复制代码

img

循环中使用块级声明

咱们都知道使用 var 声明的变量是不存在块级做用域的,即在 if/for 的 {} 做用域内使用 var

声明的变量实际上是该全局做用下的全局变量。

好比:咱们常见的 for 循环中的 i 的值

for (var i = 0; i < 10; i++) {
  // ...
}

console.log(i) // 10
复制代码

+RESULTS:

10
复制代码

结果为 10 代表在 console.log(i) 处是能够访问 i 变量的,由于 var i = 0; 的声明

被提高成了全局变量,即循环体中使用的一直是这一份全局变量。

若是是同步代码,可能没什么问题,但要是异步代码就会出现问题,以下结果:

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

+RESULTS:

5
5
5
5
5
复制代码

很遗憾最后结果都成了 5,由于循环体是个异步代码 setTimeout

解决方法有:

  • 闭包:

造成一个封闭的做用域,将当前的 i 值传递进去。

for (var i = 0; i < 5; i++) {
  (v => {
    // 这里的 v 值即传递进来的当前次循环的 i 的值
    setTimeout(() => console.log(v))
  })(i)
}
复制代码

+RESULTS:

0
1
2
3
4
复制代码
  • let

每次循环至关于新建立了一个变量,所以变量的值都得以保存。

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

+RESULTS:

0
1
2
3
4
复制代码

全局做用域声明

var, let, const 另外一个区别是在全局环境下的声明做用域也是不同,

咱们都知道在全局做用域下使用 var 声明的话,浏览器端是能够经过 window.name 来访问该变量的,可是 let, const 却不行。

var age = 100

let name = 'xxx'

console.log(window.name)
console.log(window.age)
复制代码

结果:

img

浏览器端做用域:

img

结论:

不管 let 在那里声明的它都是个块级做用域变量,只在其声明到该做用域以后才能使用。

var 声明的始终相对于当前做用域下是全局变量。

总结(var, let, const)

在 es6 以后尽可能使用 let 和 const 去声明变量,严格控制变量的做用域。

  1. var 变量声明会提高,可重复声明,且在该做用域内为全局变量
  2. let/const 变量声明不会提高,不可重复声明,局部变量,且在 DTZ 范围内使用即便是 typeof 也会报错
  3. let/const 区别在于 const 声明的变量值不能发生改变
关键词 提高 做用域 值属性
var 有提高,声明提高(命名函数定义也提高) 范围内全局 可变
let 无提高 局部变量,做用域内声明处开始往下 可变
const 无提高 局部变量,做用域内声明处开始往下 不可变

字符串和正则表达式

更好的 Unicode 编码支持

UTF-16 编码

新增 str.codePointAt(n) 和 String.fromCodePoint(str)

已有的编码查询函数: str.charCodeAt 和 String.fromCodeAt 用来应对单字符一个字节的状况。

新增的两个函数能够处理单个字符串占两个字节的大小,好比一些特殊字符“𠮷”须要用到两个字节来存储。

即 2bytes = 16bits 大小。

charCodeAt 和 fromCodeAt 是以一个字节为单位来处理字符串的,所以若是遇到这些字就无法正常处理。

var name = '𠮷'

console.log(name.charCodeAt(0))
console.log(name.codePointAt(0))
console.log(String.fromCharCode(name.charCodeAt(0)))
console.log(String.fromCodePoint(name.codePointAt(0)))

复制代码

+RESULTS:

55362
134071
�
𠮷
复制代码

能够看到若是咱们还用原来的函数 charCodeAt 和 fromCharCode 去处理这个字获得结果是不正确的。

normalize() 函数

参考连接:www.cnblogs.com/hahazexia/p…

repeat(n) 函数

将一个字符串重复 n 次后返回。

var c = 'x'

var b = c.repeat(3)

console.log(b, c, b === c)
复制代码

+RESULTS:

xxx x false
复制代码

正则表达式

y 标记

模板字符串

基本语法

let msg = `hello world`

console.log(msg)
console.log(typeof msg)
console.log(msg.length)
复制代码

+RESULTS:

hello world
string
11
复制代码

若是须要用到反引号,则须要使用转义字符: \`

多行字符串

避免一行太长,进行换行书写,可是不影响最终结果显示在一行,可使用反斜杠

var msg = `multiline \ string`

console.log(msg)
复制代码

+RESULTS:

multiline string
复制代码

多行字符串状况:

var msg = "multiline \n string"

console.log(msg)
复制代码

+RESULTS:

multiline
 string
复制代码

使用模板字符串,会按照模板字符串中的格式原样输出,而再也不须要显示使用 `\n` 来进行换行:

var msg = `multiline string`

console.log(msg)
复制代码

+RESULTS:

multiline
string
复制代码

在模板字符串中空格也会是字符串的一部分

var msg1 = `multiline string`

var msg2 = `multiline string`

console.log(`len1: ${msg1.length}`)
console.log(`len2: ${msg2.length}`)
复制代码

+RESULTS:

len1: 19
len2: 16
复制代码

因此在书写模板字符串的时候必须慎重使用缩进。

模板字符串插值

var name = 'xxx'
const getAge = () => 100

console.log(`my name is ${name}`) // 普通字符串
console.log(`3 + 4 = ${3 + 4}`) // 可执行计算
console.log(`call function to get age : ${getAge()}`) // 可调用函数
复制代码

+RESULTS:

my name is xxx
3 + 4  = 7
call function to get age : 100
复制代码

标签模板

容许使用标签模板,该标签对应的是一个函数,后面的模板字符串会被解析成参数传递给该函数去进行处理,最后返回处理的结果。

好比: let msg = tag`Hello World`

定义标签:

function tag(literals, ...substitutions) {
  // 返回一个字符串
}
复制代码

示例:

let count = 10,
    price = 0.25,
    msg = passthru`${count} items cost $${(count * price).toFixed(2)}.`

function passthru(literals, ...subs) {
  console.log(literals.join('--'))
  console.log(subs)

  // 将结果拼起来

  return subs.map((s, i) => literals[i] + subs[i]).join('')
    + literals[literals.length - 1]
}

console.log(msg)
复制代码

+RESULTS:

-- items cost $--.
[ 10, '2.50' ]
10 items cost $2.50.
复制代码

从结果能够看到,标签函数参数的内容分别为:

  1. literals 被插值({})分割成的字符串数组,好比上例的结果为: `["", " items const", "."]`
  2. subs 为插值计算的结果值做为第2, … 第 n 个参数传递给了 passthru

标签模板原始值(String.raw())

有时候须要在模板字符串中直接使用带有转义字符的内容,好比: `\n` 而不是使用其转义以后的含义。

这个时候则可使用新增的内置 tag 函数来处理。

好比:

let msg1 = `multiline\nstring`
let msg2 = String.raw`multileline\nstring`

console.log(msg1)
console.log(msg2)
复制代码

+RESULTS:

multiline
string
multileline\nstring
复制代码

可看到在咱们使用 String.raw 以后的 \n 并无被转义成换行符,而是按照其原始的样子输出。

若是在不适用内置的 Strng.raw 该怎么作?

function raw(literals, ...subs) {

  // 将结果拼起来

  return subs.map((s, i) => literals.raw[i] + subs[i]).join('')
    + literals.raw[literals.length - 1]
}

let msg = raw`multiline\nstring`

console.log(msg)

复制代码

+RESULTS:

multiline\nstring
复制代码

nodejs 环境可能看起来不直观,经过下图咱们来直观的查看下标签函数是怎么处理带转义字符的字符串的:

img

会发现其实 literals 的值依旧是转义以后的,看数组中第一个元素的字符串中是有一个回车标识的。

此外该数组对象自己上面多了一个 raw 属性,其值为没有转义的内容。

从这里咱们得出,标签模板是怎么处理带转义字符串的模板的。

总结

  1. 完整的编码支持赋予了 JavaScript 处理 UTF-16 字符的能力(经过 codePointAt()String.fromCodePoint() 来转换)
  2. u 新增的标记使得正则表达式能够经过码点来代替 UTF-16 字符
  3. normalize()
  4. 模板字符串,支持原始字符串,插值支持计算表达式或函数调用
  5. 标签模板,第一个参数为分割后的字符串列表,后面的参数分别为插值结果
  6. 转义标签模板,转义标签的第一个参数数组对象上包含一个 raw 数组,其中包含了原始值列表

函数

参数默认值

function makeRequest(url, timeout = 2000, callback = () => {}) {
  // ...
}
复制代码

默认参数值是如何影响 arguments 对象的?

严格非严格模式下的 arguments

只要记住一旦使用了默认值,那么 arguments 对象的行为将发生改变。

在 ECMAScript5 的非严格模式下,arguments 对象的内容是会随着函数内部函数参数值得变化而发生变化的,也就是说它

并非在调用函数之初值就固定了,好比:

function maxArgs(first, second) {
  console.log(first === arguments[0])
  console.log(second === arguments[1])
  first = 'c'
  second = 'd'
  console.log(first === arguments[0])
  console.log(second === arguments[1])
}

maxArgs('a', 'b')
复制代码

+RESULTS:

true
true
true
true
复制代码

从结果咱们会发现,参数值发生变化也会致使 arguments 对象跟着变化,这种状况只会在非严格模式下产生,

在严格模式下, arguments 对象是不会随着参数值改变而改变的。

function maxArgs(first, second) {
 'use strict';

  console.log(first === arguments[0])
  console.log(second === arguments[1])
  first = 'c'
  second = 'd'
  console.log(first === arguments[0])
  console.log(second === arguments[1])
}

maxArgs('a', 'b')

复制代码

+RESULTS:

true
true
false
false
复制代码

喏,后面结果为 false

带默认参数值状况下 arguments

在 es6 以后,arguments 的行为和以前严格模式下是同样的,即不会映射参数值得变化。

  1. 带默认值得参数,若是在调用的时候不传递,是不会计入到 arguments 对象当中

    即 arguments 的实际个数是根据调用的时候所传递的参数个数来决定的。

  2. arguments 对象再也不响应参数值得变化

function mixArgs(first, second = 'b') {
  console.log(arguments.length)
  console.log(first === arguments[0]) // true
  console.log(second === arguments[1]) // false
  first = 'c'
  second = 'd'
  console.log(first === arguments[0]) // false
  console.log(second === arguments[1]) // false
}

mixArgs('a')
复制代码

+RESULTS:

1
true
false
false
false
复制代码

默认参数表达式

参数默认值不只可使用静态值,还能够赋值为调用函数的结果

function getValue() {
  console.log('get value...')
  return 5
}

function add(first, second = getValue()) {
  return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 6
复制代码

+RESULTS:

2
get value...
6
复制代码

从结果显示:

  1. 若是 second 没传,会在调用 add() 时候执行 getValue() 获取默认值
  2. 若是传递了 second,那么 getValue() 是不会被执行的

即在默认参数中调用的函数,是由在调用时该对应的函数参数是否有传递来决定是否调用。

而不是传递了 second,先调用 getValue() 获得值,而后用传递的 second 值去覆盖。

也就是说 getValue() 返回的值不用每次都同样,是能够在每次调用的时候发生变化的,好比:

var n = 5

function getValue() {
  return n++
}

function add(first, second = getValue()) {
  return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 6
console.log(add(1)) // 7
复制代码

+RESULTS:

2
6
7
复制代码

因为上面的特性,参数默认值能够是动态的,所以咱们能够将前面参数值做为后面参数的默认值来使用,

好比:

function add(first, second = first) {
  return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 2
复制代码

+RESULTS:

2
2
复制代码

甚至还能够将 first 做为参数传递给 getValue(first) 获取新值做为默认值来用。

默认参数值的临时死区(TDZ)

这里临时死区的意思是指,第二个参数在使用以前未进行声明,由于参数的声明至关于使用了 let

根据 let 的特性,在为声明以前使用属于在 TDZ 范围,会抛异常。

实例:

function add(first = second, second) {
  return first + second
}

console.log(add(1, 1)) // 2

try {
  add(undefined, 1) // error
} catch (e) {
  console.log(e.message)
}
复制代码

+RESULTS:

2
second is not defined
复制代码

既然都存在 TDZ 那为何第一次调用就没事了,下面来分析下看看:

记住上一节所讲的:

默认值的调用(如: getValue() )只有在参数未传递的状况下才会发生,这里 first=second 的状况依旧适用。

那么将这句话应用到这里:

  1. add(1, 1) 这里 first 传递了 1

    那么 first 在 add 被调用的时候会被初始化成 1,根据上面那句话,即此时 first=second 这句至关于并无被执行

    所以就不会去检测 second ,也就不会出现未定义了,从而能得出正确结果:2。

  2. add(undefined, 1) 传递了 `undefined` 至关于没传这个参数,只是占了个位

    那么既然没传, first=second 就会被执行, second 就会被检测是否认义,然而检测的结果就是“未定义”,

    所以抛出异常。

将 add 函数参数的变化用下来转声明来表示,问题就会更明显了:

// add(1, 1)

let first = 1 // first = second 未执行,不检测
let second = 1

// add(undefined, 1)
let first = second // 这句被执行,至关于这里提早使用了 second 变量,let 特性生效
let second = 1
复制代码

{% note warning %}
函数参数是有它本身的做用域和TDZ的,而且和函数体做用域是区分开的,

这就意味着函数参数是没法访问函数体内的任何变量的,由于根据就是两个不一样的做用域。
{% endnote %}

未命名参数

为何会存在未命名参数?

由于 JavaScript 是没有限制调用函数的时候传递参数个数的。

好比:声明了一个函数 function add() {} 没任何参数,可是调用的时候是能够这样的 add(1, 2, 3, ...)

那么这些调用的时候传递给 add 的参数对应的函数参数就叫作未命名参数。

function add() {
  let n = 0
  ;[].slice.call(arguments).forEach(v => n += v)

  return n
}

console.log(add(1, 2, 3, 4, 5))
复制代码

+RESULTS:

15
复制代码

参数展开符(…)

未命名参数通常不多使用,由于这让使用者会很迷惑该函数的做用,所以参数没任何明显特征表示它是干什么用的,

在 es6 中增长了一个展开符号(…),在函数参数中的做用是将传递进的参数列表合并成一个参数数组。

适用于一个函数参数个数未知的状况下使用。

好比:

function pick(object, ...keys) {
  // 这里 keys 会成为一个包含传入的其他参数值的数组
  let result = Object.create(null)

  console.log(arguments.length)
  for (let i = 0; i < keys.length; i++) {
    result[keys[i]] = object[keys[i]]
  }

  return result
}

const book = {
  author: 'xxx',
  name: 'yyy',
  pages: 300
}

const res = pick(book, 'author', 'name')

console.log(JSON.stringify(res))
复制代码

+RESULTS:

3
{"author":"xxx","name":"yyy"}
复制代码

利用 …keys 将传入的 ('author', 'name') 合并成了一个数组: ['author', 'name'] ,方便应对

函数参数个数可变的状况。

参数展开符两种异常使用状况

  1. 展开符参数必须是最后一个,不能在其后面还有其余参数

    好比: function add(n, ...vals, more) {} 这会出现异常

  2. 不能用在对象的 setter 函数上

实例:

const obj = {
  set name(...val) {}
}
复制代码

img

function add(n, ...vals, more) {

}
复制代码

img

参数展开符对 arguments 的影响

记住一点:

arguments 老是由函数调用时传递进来的参数决定

function checkArgs(...args) {
  console.log(args.length);
  console.log(arguments.length);
  console.log(args[0], arguments[0]);
  console.log(args[1], arguments[1]);
}

checkArgs("a", "b");
复制代码

+RESULTS:

2
2
a a
b b
复制代码

函数构造函数能力加强

在实际编码过程,咱们不多直接使用 Function() 构造函数去建立一个函数。

好比这么使用:

// 参数:参数一名称 first, 参数二名称 second,... 最后一个是函数体
var add = new Function('first', 'second', 'return first + second')

console.log(add(1, 2))
复制代码

+RESULTS:

3
复制代码

在 es6 中对构造函数的使用能力加强了,给其赋予了更多的功能,好比

  1. 默认参数值
  2. 展开符
var add = new Function("first", "second = first",
                       "return first + second");

console.log(add(1, 1));     // 2
console.log(add(1));        // 2

var pickFirst = new Function("...args", "return args[0]");

console.log(pickFirst(1, 2));   // 1
复制代码

+RESULTS:

2
2
1
复制代码

展开符(…)

在以前咱们在函数参数中用到了展开符,这个时候的用途是将参数合并成数组来用。

普通参数传递

咱们通常调用函数的时候都是将参数逐个传递:

let v1 = 20,
    v2 = 30

console.log(Math.max(v1, v2))
复制代码

+RESULTS:

30
复制代码

这仅仅两个参数,比较好书写,一旦参数多了起来就比较麻烦,在 es6 以前的作法能够利用 Function.prototype.apply 去实现:

apply 传递多个参数

let vs = [1, 2, 3, 4, 5]

console.log(Math.max.apply(Math, vs))
复制代码

+RESULTS:

5
复制代码

由于 apply 会将数组进行展开做为函数的参数传递个调用它的函数。

es6 以后展开符传递

在 es6 以后咱们将使用展开符去完成这项工做,让代码更简洁和便于理解。

let vs = [1, 2, 3, 4]

console.log(Math.max(...vs))
复制代码

+RESULTS:

4
复制代码

展开符,传统方式相结合

let vs = [1, 2, 3, 4]

console.log(Math.max(10, ...vs)) // 10
console.log(Math.max(...vs, 0)) // 4
console.log(Math.max(3, ...vs, 10)) // 10
复制代码

+RESULTS:

10
4
10
复制代码

函数名字属性

以往,因为函数的各类使用方式使 JavaScript 在识别函数的时候成为一种挑战,而且匿名函数的

频繁使用使得程序的 debugging 过程异常痛苦,常常形成追踪栈很难理解。

所以在 es6 中给全部函数添加了一个 name 属性。

{% note info %}
name 属性只是对函数的一种描述特性,并不会有实际的引用特性,也就是说

在实际编程中不可能经过函数的 name 属性去干点啥。
{% endnote %}

选择合适的名称

JavaScript 会根据函数的声明方式去给其选择合适的名称,好比:

function doSomething() {
  // ...
}

var doAnotherThing = function() {
  // ...
};

var doThirdThing = function do3rdThing() {

}

console.log(doSomething.name);          // "doSomething"
console.log(doAnotherThing.name);       // "doAnotherThing"
console.log(doThirdThing.name);       // "do3rdThing"
复制代码

+RESULTS:

doSomething
doAnotherThing
do3rdThing
复制代码
  1. 若是是命名函数式声明方式,则使用的就是它的名字做为 name 属性值,如: doSomething

  2. 若是是表达式匿名方式声明函数,则将使用表达式中左边的变量名称来做为 name 属性值,如: doAnotherThing

  3. 表达式命名方式声明函数,则将使用命名函数的名称做为 name 属性,如: doThridThing 的名字是: do3rdThing

{% note info %}
经过第三个输出可知,命名函数的优先级高于表达式的变量名。
{% endnote %}

name 属性的特殊状况

  1. 对象的函数名称,即该函数的名字
  2. 对象的访问器函数名称,经过 Object.getOwnPropertyDescriptor(obj, 'keyname') 获取访问器对象
  3. 调用 bind() 以后的函数名称,老是在原始函数名前加上 bound
  4. 使用 new Function() 建立的函数名称,老是返回 anonymous
var doSth = function() {}

var person = {
  get firstName() {
    return 'Nicholas'
  },

  sayName: function() {
    console.log(this.name)
  }
}

console.log(person.sayName.name) // sayName
// 访问器属性,只能经过 getOwnPropertyDescriptor 去获取
var descriptor = Object.getOwnPropertyDescriptor(person, 'firstName')
console.log(descriptor.get.name) // get firstName

// 调用 bind 以后的函数名称老是会在原始的函数名称以前加上 `bound fname`
console.log(doSth.bind().name) // bound doSth
console.log((new Function()).name) // anonymous
复制代码

+RESULTS:

sayName
get firstName
bound doSth
anonymous
复制代码

澄清函数双重目的

函数使用方式

  1. 直接调用,当作函数来使用 Person()

  2. 使用 new 的时候当作构造函数来使用建立一个实例对象

在 es6 以后为了搞清楚这两种使用方式,添加了两个内置属性: [[Call]][[Constructor]]

当当作函数直接调用时,其实内部是调用了 [[Call]] 执行了函数体,

当结合 new 来使用是,调用的是 [[Contructor]] 执行了如下步骤:

  1. 建立一个新的对象 newObj

  2. this 绑定到 newObj

  3. 将 newObj 对象返回做为该构造函数的一个实例对象

也就是说咱们能够在构造函数中去改变它的行为,若是它没有显示的 return 一个合法的对象,

则会默认走 #3 步,若是咱们显示的去返回了一个对象,那么最后获得的实例对象即这个显示返回的对象。

function Person1(name) {
  this.name = name || 'xxx'
}

// 没有显示的 return 一个合法对象
// 返回的是新建立的对象,而且 this 被绑定到这个心对象上
const p1 = new Person1('张三')

// 所以这里访问的 name 即构造函数中的 this.name
console.log(p1.name)

function Person2(name) {
  this.name = name || 'xxx'

  return {
    name: '李四'
  }
}

// 按照构造函数的使用定义,这里返回的是
// 显示 return 的那个对象: { name: '李四' }
const p2 = new Person2('张三')

// 所以这里输出的结果为:李四
console.log(p2.name)
复制代码

+RESULTS:

张三
李四
复制代码

{% note warning %}
并非全部的函数都有 [[Constructor]] ,好比箭头函数就没有,所以箭头函数

也就不能被用来 new 对象实例。
{% endnote %}

判断函数被如何使用?

有时候咱们须要知道函数是如何被使用的,是当作构造函数?仍是单纯当作函数直接调用?

这个时候 instanceof 就派上用场了,它的做用是用来检测一个对象是否在当前对象的

原型链上出现过。

好比:在 es5 中强制一个函数只能当作构造函数来使用,通常这么作

function Person(name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('必须使用 new 来建立实例对象。')
  }
}

var person = new Person('张三')

// 这种调用,内部的 `this` 被绑定到了全局对象
// 而全局对象并不是 Person 原型链上的对象,所以会
// 执行 else 抛出异常
var notAPerson = Person('李四')
复制代码

img

可是有一种直接调用的状况,不会走 else ,即经过 call 调用指定 person 实例为调用元。

function Person(name) {
  if (this instanceof Person) {
    this.name = name
  } else {
    throw new Error('必须使用 new 来建立实例对象。')
  }
}

var person = new Person('张三')

// 这样是合法的,请 this instanceof Person 成立
// 由于 Person.call(person, ...) 指定了做用域为实例对象 person
// 所以函数内部的 this 会被绑定到这个实例对象 person 上,
// 而 person 确实是 Person 的实例对象,所以不会报错
var notAPerson = Person.call(person, '李四')

复制代码

正常运行的结果

+RESULTS:

undefined
复制代码

所以,若是是 Person.call(person, ...) 这种状况调用,函数内部一样没法判断它的被使用方式是如何。

new.target 元属性

为了解决上一节的“函数调用方式”判断的问题, es6 中引入了 new.target 元属性。

{% note info %}
元属性:一个非对象的属性,用来为他的目标(好比: new )提供额外的相关信息。
{% endnote %}

new.target 的取值??

  1. 若是函数当作构造函数

    使用 new 来调用,内部调用 [[Constructor]] 的时候, new.target 会被填充为 new 操做符

    指定的目标对象,这个目标对象一般是执行内部构造函数的时候新建立的那个对象实例(在函数体重通常是 this )。

  2. 若是函数当作普通函数直接调用,那么 new.target 的值为 undefined

从上面两点,那么咱们就能够经过在函数内部判断 new.target 来判断函数的使用方式了。

function Person(name) {
  if (typeof new.target !== 'undefined') {
    this.name = name
  } else {
    throw new Error('必须使用 new 建立实例。')
  }
}

var person = new Person('张三')
console.log(person.name, 'new')

var notAPerson = Person.call(person, '李四')
console.log(notAPerson.name, 'call')
复制代码

img

由图中的输出证实上面 #1 和 #2 的结论,也由此结论咱们能够直接使用 new.target === Person 做为断定条件。

函数外部使用 new.target :

function Person() {

}

if (new.target === Person) {
  // ...
}
console.log(new.target)
复制代码

块级函数

<= es3 行为

在 es3 或更早些时候,在块级做用域中声明函数会出现语法错误,虽然在以后默认容许这样使用(不会报错了),可是

各个浏览器之间的处理方式依旧不一样,所以在实际开发过程当中,应该尽可能避免这么使用,若是非要在块级做用域声明函数

能够考虑使用函数表达式方式。

es5 行为

另外,为了尝试去兼容这种怪异状况,在 es5 的严格模式下若是在块级做用域声明函数,会爆出异常。

'use strict';

if (true) {
  // 在 es5 中会报语法错误, es6 中不会
  function doSth() {}
}
复制代码

es6 行为

在 es6 以后,这种函数声明将会变的合法,且声明以后 doSth() 就成了一个局部函数变量,即

只能在 if (true) { ... } 这个做用域内部访问,外部没法访问,好比:

'use strict';

if (true) {
  // 由于有提高,且命名函数的提高包含声明和定义都会被提高
  console.log(typeof doSth) // function
  function doSth() {}

  doSth()
}

// es6 以后存在块级做用域,所以 doSth 是个局部变量,在
// 它的做用域范围以外没法访问
console.log(typeof doSth); // undefined
复制代码

+RESULTS:

function
undefined
复制代码

决定何时该用块级函数

4.7.3 一节中使用的是命名式函数声明方式,这种方式声明和定义均被提高,所以在

声明处至上访问能获得正常结果。

若是使用表达式 + let 方式,则结果会和用 let 声明同样存在 TDZ 的问题。

'use strict';

if (true) {
  // TDZ 区域,访问会异常
  console.log(typeof doSth) // error

  let doSth = function () {}

  doSth()
}

console.log(typeof doSth) // undefined
复制代码

img

所以,咱们能够根据需求去决定该使用哪一种方式去声明块级函数,若是须要有提高则应该使用“命名式函数”,

若是不须要提高,只须要在声明以后的范围使用应该使用“函数表达式”方式去声明函数。

非严格模式块级函数

在 es6 中的非严格模式下,块级函数的提高再也不是针对块级做用域,而是函数体或全局环境。

// 至关于提高到了这里

if (true) {
  console.log(typeof doSth)

  // 非严格模式,全局提高
  function doSth() {}

  doSth()
}

console.log(typeof doSth) // function
复制代码

+RESULTS:

function
function
复制代码

结果显示外面的 typeof doSth 也是 'function' 。

所以,在 es6 以后函数的声明只须要区分严格或非严格模式,而再也不须要考虑浏览器的兼容问题,至关于统一了标准。

箭头函数

箭头函数特性

在 es6 中引入了箭头函数,大大的简化了函数的书写,好比

声明一个函数: function run() {}

如今: const run = () => {} 或者 const getName = () => '张三'

虽然用起来方便了,可是箭头函数与普通函数又很大的不一样,使用的时候必需要注意如下几点:

特性 说明
1 this 减小问题,便于优化
2 super
3 arguments 箭头函数必须依赖命名参数或 rest 参数去访问函数的参数列表
4 new.target 元属性 不能被实例化,功能无歧义,不须要这个属性
5 不能 new 实例化
6 无原型 由于不能用 new 所以也不须要原型
7 不能改变 this 指向 此时指向再也不受函数自己限制
8 不能有重复的命名参数 以前非严格模式下普通函数是能够有的

{% note info %}
箭头函数中若是引用 arguments ,它指向的再也不是该箭头函数的参数列表,

而是包含该箭头函数的那个非箭头函数的参数列表(4.8.6)。
{% endnote %}

没有 this 绑定主要有两点理由:

  1. 不易追踪,易形成未知行为,众多错误来源

    函数内部 this 的值很是不容易追踪,常常会形成未知的函数行为,箭头函数去掉它能够避免这些烦恼

  2. 便于引擎优化

    限制箭头函数内部使用 this 去执行代码也有利于 JavaScript 引擎更容易去优化内部操做,而不像

    普通函数同样,函数有可能会当作构造函数使用或其余用途。

{% note info %}
一样,箭头函数也有本身的 name 属性,用来描述函数的名称特征。
{% endnote %}

const print = msg => {
  console.log(arguments.length, 'arguments')
  console.log(this, 'this')
  console.log(msg)
}

console.log(print.name)

print('...end')
复制代码

+RESULTS:

print
0 'arguments'
Object [global] {
// ... 省略
        { [Function: setImmediate] [Symbol(util.promisify.custom)]: [Function] } } 'this'
...end
undefined
复制代码

由于是 nodejs 环境,所以 this 被绑定到了 global 对象上。

第二行输出结果是 0 'arguments' 说明已经不能使用 arguments 去正确获取传入的参数了。

箭头函数语法

箭头函数语法很是灵活,具体如何使用根据使用场景和实际状况决定。

好比:

var reflect = value => value; 直接返回原值

至关于

var reflect = function(value) { return value; }

当只有一个参数时刻省略小括号 ()

多个参数时候:

var sum = (n1, n2) => n1 + n2;

函数体更多内容时候:

var sum = (n1, n2) => {
  // do more...
  return n1 + n2;
}
复制代码

空函数:

var empty = () => {}

返回一个对象:

var getTempItem = id => ({ id: id, name: 'Temp' })

等等。。。

箭头当即函数表达式

在 es6 以前咱们要实现一个当即执行函数,通常这样:

let person = function(name) {
  return {
    getName: function() {
      return name
    }
  }
  // 直接在函数后面加上小括号即成为当即执行函数
}('张三')

console.log(person.getName()) // 张三
复制代码

+RESULTS:

张三
复制代码

PS: 可是为了代码可读性,建议给函数加上小括号。

箭头函数形式的当即执行函数,不能够直接在 } 后面使用小括号方式:

let person = ((name) => {
  return {
    getName: function() {
      return name
    }
  }
})('张三')


console.log(person.getName()) // 张三
复制代码

+RESULTS:

张三
复制代码

没有 this 对象

在以前咱们常常遇到的一个问题写法是事件的监听回调函数中直接使用 this ,这将致使引用错误问题,

由于事件的回调属于被动触发的,而触发调用该回调的对象是不肯定的,这就会致使各类问题。

var PageHandler = {

  id: "123456",

  init: function() {
    document.addEventListener("click", function(event) {
      // 这里用了 this ,意图是想在点击事件触发的时候去调用 PageHandler 的
      // doSomething 这个函数,但实际倒是事与愿违的
      // 由于这里的 this 并不是指向 Pagehandler 而是事件触发调用回调时候的那个目标对象
      this.doSomething(event.type);     // error
    }, false);
  },

  doSomething: function(type) {
    console.log("Handling " + type  + " for " + this.id);
  }
};
复制代码

以往解决方法:经过 bind(this) 手动指定函数调用对象

var PageHandler = {

  id: "123456",

  init: function() {
    // 通过 bind 以后,回调函数的调用上下文就被绑定到了 PageHandler 这个对象
    // 真正绑定到 click 事件的函数实际上是执行 bind(this) 以后绑定了上下文的一个函数副本
    // 从而执行能获得咱们想要的结果
    document.addEventListener("click", (function(event) {
      this.doSomething(event.type);     // no error
    }).bind(this), false);
  },

  doSomething: function(type) {
    console.log("Handling " + type  + " for " + this.id);
  }
};

复制代码

虽然问题是解决了,可是使用 bind(this) 无疑多建立了一份函数副本,多少都会有些奇怪。

而后,在 es6 以后这个问题就很好的被箭头函数解决掉:

根据箭头函数没有 this 绑定的特性,在其内部使用 this 的时候这个指向将是包含该箭头函数的非箭头函数

所在的上下文,即:

var PageHandler = {

  id: "123456",

  init: function() {
    document.addEventListener(
      "click",
      // 箭头函数无 this 绑定,内部使用 this
      // 这个 this 的上下文将有包含该箭头函数的上一个非箭头函数
      // 这里即 init() 函数,而 init() 函数的上下文为 PageHandler 对象
      // 也就是说这里箭头函数内部的 this 指向的就是 Pagehandler 这个对象
      // 从而让代码按照预期运行
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log("Handling " + type  + " for " + this.id);
  }
};
复制代码

箭头函数和数组

在使用数组的一些内置函数时,咱们常常会碰到须要传递一个参考函数给他们,好比,排序函数 Array.prototype.sort 就须要

咱们传递一个比较函数用来决定是升序仍是降序等等。

若是用箭头函数将大大简化代码:

// es6 以前
const values = [1, 10, 2, 5, 3]

var res1 = values.sort(function(a, b) {
  // 指定为升序
  return a - b;
})

// es6 以后
var res2 = values.sort((a, b) => a - b)
console.log(res1.toString(), res2.toString())

复制代码

+RESULTS:

1,2,3,5,10 1,2,3,5,10
复制代码

或者 map(), reduce() 等等用起来会更方便更简洁许多。

无参数绑定(arguments)

看实例:

function createArrowFunctionReturningFirstArg() {
  return () => arguments[0]
}

var arrowFunction = createArrowFunctionReturningFirstArg(5)

console.log(arrowFunction()) // 5
复制代码

+RESULTS:

5
复制代码

从结果看出,返回的 arrowFunction() 箭头函数调用的时候并无传递任何参数,可是执行结果获得告终果

这个结果正是包含它的那个非箭头函数(createArrowFunctionReturingFirstArt())所接受的参数值。

所以箭头函数内部若是访问 arguments 对象,此时该对象指向的是包含它的那个非箭头函数的参数列表对象。

箭头函数的识别

跟普通函数同样, typeofinstanceof 对齐依然使用。

var comparator = (a, b) => a - b;

console.log(typeof comparator) // function
console.log(comparator instanceof Function) // true
复制代码

+RESULTS:

function
true
复制代码

4.8.1 一节提到过箭头函数是不能改变 this 指向的,可是

并不表明咱们就彻底不能使用 call, apply, bind

好比:

var sum = (n1, n2) => (this.n1 || 0) + n2

console.log(sum.call(null, 1, 2)) // 3
console.log(sum.call({ n1: 10 }, 1, 2)) // 3
复制代码

+RESULTS:

2
2
复制代码

从这个例子中能够验证,箭头函数是没法修改它的 this 指向的,若是能够修改

第二个结果值就应该是 12 而不是和第一个同样为 2 ,由于在第二个中

咱们手动将 sum 执行上下文绑定到了一个新的对象上 {n1: 10}

{% note warning %}
也就是说,并不是不能使用,而是用了也不会有任何变化而已。
{% endnote %}

使用 bind 保留参数:

var sum = (n1, n2) => n1 + n2

console.log(sum.call(null, 1, 2)) // 3
console.log(sum.apply(null, [1, 2])) // 3

// 产生新的函数,这种和普通函数使用方式同样
var boundSum = sum.bind(null, 1, 2)

console.log(boundSum())
复制代码

+RESULTS:

3
3
3
复制代码

尾调用优化

尾调用:将一个函数的调用放在两一个函数的最后一行。

或许在 es6 中对于函数相关的最感兴趣的改动就是引擎的优化了,它改变了函数的尾调用系统。

function doSth() {
  return doSthElse() // tail call
}
复制代码

在 es6 以前,它和普通的函数调用同样被处理:建立一个新的栈帧而后将它推到调用栈的栈顶等待被执行

也就意味着以前的每个栈帧都在内存里面保留着,若是调用栈过大那这将多是问题的来源。

有什么不一样?

在 es6 以后优化了引擎,包含尾调用系统的优化(严格模式下,非严格模式下依旧未发生改变)。

优化以后,再也不会为尾部调用建立一个新的栈帧,而是将当前的栈帧状况,而后将其复用到尾部调用,前提是知足下面几个条件:

  1. 尾调用函数不须要访问当前栈帧中的任何变量(即尾调用的函数不能是闭包,闭包的做用就是用来持有变量)

  2. 即在尾调用的函数以后不能有其余的代码,即尾调用函数必须是函数体的最后一行

  3. 尾调用函数的调用结果要做为当前函数的返回值返回

好比:下面的函数就知足尾调用优化的条件

'use strict'; // 1. 严格模式

function doSth() {

  // 2. 没有引用任何内部变量,非闭包

  // 3. 最后一行

  // 4. 调用结果被做为 doSth 的返回值返回
  return doSthElse()
}
复制代码

如下状况不会被优化:

'use strict';

function doSth() {
  doSthElse() // 返回做为返回值,不会优化
}

function doSth1() {
  return 1 + doSthElse() // 在尾调用函数返回以后不能有其余操做,不会优化
}

function doSth2() {
  var res = doSthElse()
  return res // 不是最后一行,即不是将结果当即返回,不会优化
}

function doSth3() {
  var num = 1,
      func = () => num

  return func() // 闭包,不会优化
}
复制代码

如何利用尾调用优化?

尾调用最经典的莫过于递归调用了,好比斐波那契数列问题。

function factorial(n) {

  if (n <= 1) {
    return 1;
  } else {

    // 不会被优化,由于函数返回以后还须要进行乘积计算才返回
    return n * factorial(n - 1);
  }
}

console.log(factorial(10))
复制代码

+RESULTS:

3628800
复制代码

上面的并不会被优化,由于尾调用函数并非当即返回的,修改以下:

function factorial(n, p = 1) {

  if (n <= 1) {
    return 1 * p;
  } else {

    let res = n * p
    // 被优化
    return factorial(n - 1, res);
  }
}


console.log(factorial(10))
复制代码

+RESULTS:

3628800
复制代码

尾调用优化应该是咱们在书写代码的时候时常应该考虑的问题,尤为是书写递归的时候,当使用递归涉及到大量的计算的时候,

尾调用优化的优点将会很明显。

总结

选项 功能 描述 其余
arguments
ES6以前非严格模式 值会随着函数体内参数的改变而改变
ES6以前严格模式 不会响应改变,调用之初就定了
ES6以后行为统一 不会响应改变,内容由实际调用者传递个数决定
函数默认参数 能够是常量值 function add(f, s = 3) {}
能够是变量 var n = 10; function add(f, s = n) {}
能够是函数调用 function getVal() {}; function add(f, s = getVal) {}
默认值参数的执行 调用时有传递则不会检测或执行,未传递则会检测和执行
相互引用 后面的参数能够引用前面的参数变量 function add(f, s = f) {}
临时死区(TDZ)
参数 rest 符号 接受多个参数,合并成数组供函数内部使用 function add(f, ...a) {}
异常使用一 不能用在访问器函数 obj = { set name(...val) {} } 非法。
异常使用二 必须做为函数最后一个参数使用 function add(f, ...s, t) {} 非法。
对arguments影响 非箭头函数没什么影响 arguments老是由调用者传递的参数决定个数
构造函数 new Function() 可使用默认值,rest符号等功能
展开符(…) 普通多参数函数 Math.max(1, 2, 3, 4, ...)
普通多参数函数apply Math.max.apply(Math, [1, 2, 3, 4])
ES6展开符 Math.max(...[1, 2, 3, 4, ...])
name 属性 函数名称 仅辅助描述功能,易于跟踪函数
特殊状况: 访问器函数 get fnName
特殊状况:bind() 函数 bound fnName
特殊状况:new Function() 匿名函数 anonymous
new.target 函数可直接调用可new构造实例 所以形成函数内部如何识别使用释放问题?
若是做为函数调用 [[Call]] new.target = undefined
若是是 new 构造函数 [[Constructor]] new.target = Person 构造函数自己
块级函数 在 es6之情块级函数的声明处理并无统一 严格模式必出异常,非严格很差说
es6以后统一标准 严格模式:块级函数只是局部函数 只在做用域内有效
非严格模式:块级函数会提高到函数顶部或全局环境 全局或函数体生效
箭头函数特性 this 不易追踪,易于引擎优化 内部可使用,可是它指向的是当前箭头函数所在的非箭头函数所在的上下文
super 没有原型,继承等,不须要 super
arguments 内部访问的该对象,实际上是当前环境函数的参数,而非箭头函数自己的参数列表
new.target 不支持 new 就不存在使用方式问题
无原型 不支持 new
不能改变 this 指向 其内部的 this 已经不是它管辖,能够调用 call, apply, bind 之流,可是不会有任何做用
不能有重复命名参数 非严格模式下ES6以前的普通参数能够用
箭头函数语法 使用方式灵活多变
当即表达式 必须括号包起来再执行,普通函数可直接在 } 后执行 (() => {})(), function(name){}('xxx')
typeof, instanceof 对箭头函数依旧有效, typeof fn = 'function', fn instanceof Function (true)
尾调用优化 必须知足三个条件 不知足条件不会优化,典型的递归调用
1. 非闭包,尾函数体内不能访问正函数体内任何变量
2. 结果值必须当即返回,不能参与其余计算后再返回
3. 必须是正函数的最后一个语句
优化以前 尾函数新建栈帧,放在调用栈顶等待调用
优化以后 清空调用栈,将它做为尾调用函数的栈帧复用

对象扩展

对象分类

类型 说明
普通对象(Ordinary) 拥有全部对象的默认行为
异类对象(Exotic) 和默认行为有所差别
标准对象(Standard) 那些由 ECMAScript 6 定义的,如: Array, Date 等等
内置对象(Built-in) 脚本当前执行环境中的对象,全部标准对象都是内置对象

对象字面量(literal)语法扩展

字面量语法在 JavaScript 中使用很是广泛

  1. 书写方便
  2. 简洁易懂
  3. JSON 就是基于字面量语法演变而来

es6 的来到是的对象字面量语法更增强大简洁易用。

对象属性简写

<= es5:

function createPerson(name, age) {
  return {
    name: name,
    age: age
  }
}
复制代码

es6:

function createPerson(name, age) {
  return {
    name,
    age
  }
}
复制代码

简洁函数写法

<= es5:

var person = {
  name: '张三',
  sayName: function() {
    console.log(this.name)
  }
}
复制代码

es6:

var person = {
  name: '张三',
  sayName() {
    console.log(this.name)
  }
}
复制代码

计算属性

在 es6 以前书写对象字面量的时候,能够直接使用多个字符串组成的字符串做为 key ,可是这种方式在实际使用中

是很是不方便的,假如说 key 是个很长的串呢??

var person = {
  'first name': '张三'
}

console.log(person['first name']) // 张三
复制代码

+RESULTS:

张三
复制代码

所以, es6 中支持了变量做为对象属性名去访问,根据变量的值动态决定使用什么 key 去访问对象的属性值,

这样无论 key 多长,只须要使用变量将它存储起来,直接使用变量名去使用将更加方便。

var person = {},
    lastName = "last name";

person["first name"] = "张三";
person[lastName] = "李四";

console.log(person["first name"]);      // "张三"
console.log(person[lastName]);          // "李四"
复制代码

+RESULTS:

张三
李四
复制代码

支持表达式计算属性名:

var suffix = ' name'

var person = {
  ['first' + suffix]: '张三',
  ['last' + suffix]: '李四'
}

console.log(person['first name']) // 张三
console.log(person['last name']) // 李四
复制代码

+RESULTS:

张三
李四
复制代码

新方法

方法 功能 其余
Object.is(value1, vlaue2) 比较两个值是不是同一个值 能弥补 === 没法判断 (+0, -0), (NaN, NaN) 问题
Object.assign(target, ...sources) 合并拷贝对象属性 自身且 enumerable: true 的属性

Object.is(value1, value2)

在以往咱们判断两个值是否相等,常用的是 ===== ,通常推荐使用后者

由于前者会有隐式强转,会在比较以前将两个值进行强制转换成同一个类型再比较。

console.log('' == false) // true
console.log(0 == false) // true
console.log(0 == '') // true
console.log(5 == '5') // true
console.log(-0 == +0) // true
console.log(NaN == NaN) // true
复制代码

+RESULTS:

true
true
true
true
true
false
复制代码

对于 +0-0 使用 === 的结果是 true ,但实际上他们是有符号的,理论上应该是不相等的。

而两个 NaN 五路你是 ===== 都断定他们是不相等的。

为了解决这些差别, es6 中加入了 Object.is() 接口,意指将等式的判断更加合理化,它的含义是

两个值是不是同一个值。

咱们看下各对值使用 Object.is() 比较的结果:

const is = Object.is
const log = console.log

// +0, -0
log('+0 == -0', +0 == -0)
log('+0 === -0', +0 === -0)
log('+0 is -0: ', is(+0, -0))

// NaN
log('NaN == NaN: ', NaN == NaN)
log('NaN === NaN: ', NaN === NaN)
log('NaN is NaN: ', is(NaN, NaN))

// number, string
log('5 == "5": ', 5 == '5')
log('5 == 5: ', 5 == 5)
log('5 === "5": ', 5 === '5')
log('5 === 5: ', 5 === 5)
log('5 is "5": ', is(5, '5'))
log('5 is 5: ', is(5, 5))
复制代码

+RESULTS:

+0 == -0 true
+0 === -0 true
+0 is -0:  false
NaN == NaN:  false
NaN === NaN:  false
NaN is NaN:  true
5 == "5":  true
5 == 5:  true
5 === "5":  false
5 === 5:  true
5 is "5":  false
5 is 5:  true
复制代码

所以, Object.is 可以弥补, === 没法判断出 +0, -0, NaN, Nan 相等的结果。

Object.assign(target, source, source1, source2, …)

参数:

  1. target 接受拷贝的对象,也将返回这个对象
  2. source 拷贝内容的来源对象
  3. 来源对象参数能够有多个,若是存在同名属性值,最后的值由最后一个拥有同名属性对象中的值为准

TC39.ECMA262 实现原理图:

img

合并对象,将 source 中自身的可枚举的属性浅拷贝到 target 对象中,返回 target 对象。

混合器(Mixins)在 JavaScript 中被普遍使用,在一个 mixin 中,一个对象能够从另个对象中

接受他们的属性和方法,即浅拷贝,许多 JavaScript 库都会有一个与下面相似的 mixin 函数:

const mixin = (receiver, supplier) => {
  Object.keys(supplier).forEach(
    key => receiver[key] = supplier[key])

  return receiver
}

function EventTarget() {}

EventTarget.prototype = {
  constructor: EventTarget,
  get name() {
    return 'EventTarget.prototype'
  },
  emit: function(msg) {
    console.log(msg, 'in EventTarget.prototype')
  },
  on: function(msg) {
    console.log(msg, 'on EventTarget.prototype')
  }
}


const myObj1 = {}
mixin(myObj1, EventTarget.prototype)

myObj1.emit('something changed from myObj1')
console.log(myObj1.name, 'obj1 name')

const myObj2 = {}
Object.assign(myObj2, EventTarget.prototype)

myObj2.on('listen from myObj1')
console.log(myObj2.name, 'obj2 name')

console.log(EventTarget.prototype, myObj1, myObj2)
复制代码

+RESULTS:

something changed from myObj1 in EventTarget.prototype
EventTarget.prototype obj1 name
listen from myObj1 on EventTarget.prototype
EventTarget.prototype obj2 name
复制代码

因为 mixin(), Object.assign 的实现都是采用的 = 操做符,所以是无法拷贝访问器属性的,或者说拷贝过来以后

就不会再是访问器属性了,看上面代码的运行结果对比图:

img

多个来源对象支持:

const receiver = {}
const res = Object.assign(receiver, {
  name: 'xxx',
  age: 100
}, {
  height: 180
}, {
  color: 'yellow',
  age: 80
})

console.log(receiver === res)
console.log(res)
复制代码

+RESULTS:

true
{ name: 'xxx', age: 80, height: 180, color: 'yellow' }
复制代码

最后 age: 80 值是最后一个来源对象中的值,返回值即第一个参数对象。

重复属性

<= es5 严格模式下,重复属性会出现语法错误:

'use strict';

var person = {
  name: 'xxx',
  name: 'yyy' // syntax error in es5 strict mode
}
复制代码

es6 不管严格或非严格模式下都属合法操做,其值为最后一个指定的值:

'use strict';

var person = {
  name: 'xxx',
  name: 'yyy' // no error
}

console.log(person.name)
复制代码

+RESULTS:

yyy
复制代码

自有属性枚举顺序

<= es5 中是不会定义对象属性的枚举顺序的,它的枚举顺序是在实际运行时取决于所处的 JavaScript 引擎。

es6 中严格定义了枚举时返回的属性顺序,这将会影响在使用 Objct.getOwnPropertyNames()

Reflect.ownKeys 时属性该如何返回。a

枚举时基本顺序遵循:

  1. 全部数字类型的 keys 为升序排序

  2. 全部字符串类型的 keys 按照它添加的时机排序

  3. 全部符号类型(Symbols)的 keys 按照它添加的时机排序

三者的优先级为: numbers > strings > symbols

var obj = {
  a: 1,
  0: 1,
  c: 1,
  2: 1,
  b: 1,
  1: 1
}

obj.d = 1

console.log(Object.getOwnPropertyNames(obj).join('')) // 012acbd
复制代码

+RESULTS:

012acbd
复制代码

{% note warning %}
因为并不是全部 JavaScript 引擎并不是统一实现方式,致使 for-in 循环依旧没法肯定枚举的顺序。

而且 Object.keys()JSON.stringify() 采用的枚举顺序和 for-in 同样。
{% endnote %}

var obj = {
  a: 1,
  0: 1,
  c: 1,
  2: 1,
  b: 1,
  1: 1
}

obj.d = 1

for (let prop in obj) {
  console.log(prop)
}
复制代码

功能更强的原型对象

原型是 JavaScript 中实现继承的基石,早起的版本中严重限制了原型能作的事情,

而后随着 JavaScript 的逐渐成熟程序员们开始愈来愈依赖原型,咱们如今能很清晰

地感觉到开发者们对原型控制上和易用性的渴望愈来愈强烈,由此 ES6 对齐进行了增强。

改变对象原型

正常状况下,对象经过构造函数或 Object.create() 建立的同时原型也就被建立了。

ES5 中能够经过 Object.getPrototypeof() 方法去获取对象原型,可是依然

缺乏一个标准的方式去获取失利以后的对象原型。

ES6 增长了 Object.setPrototypeof(source, target) 用来改变对象的原型指向,

指将 source.prototype 指向 target 对象。

let person = {
  getGreeting() {
    return "Hello";
  }
};

let dog = {
  getGreeting() {
    return "Woof";
  }
};

// prototype is person
let friend = Object.create(person);
console.log(friend.getGreeting());                      // "Hello"
console.log(Object.getPrototypeOf(friend) === person);  // true

// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());                      // "Woof"
console.log(Object.getPrototypeOf(friend) === dog);     // true
复制代码

实际上,一个对象的原型是存储在它的内部属性 [[Prototype]] 上的, Object.getPrototypeOf()

获取的也是这个属性的值, Object.setPrototypeOf() 设置也是改变这个属性的值。

旧版原型的访问

好比:若是想在实例中重写原型的某个方法的时候,须要在重写的方法内调用原型方法时候,以往是这样搞

let person = {
  getGreeting() {
    return "Hello";
  }
};

let dog = {
  getGreeting() {
    return "Woof";
  }
};


let friend = {
  getGreeting() {
    return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  }
};

// set prototype to person
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting());                      // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person);  // true

// set prototype to dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting());                      // "Woof, hi!"
console.log(Object.getPrototypeOf(friend) === dog);     // true
复制代码

经过 Object.getPrototypeOf(this).getGreeting.call(this) … 去获取原型中的方法

经过 super 引用简化原型的访问

如以前所提,原型是 JavaScript 中一个很重要也很经常使用的一个对象,ES6 对他们的使用进行了简化。

另外 es6 对原型的另外一个改变是 super 的引用,这让对象访问原型对象更加方便。

而在 es6 增长 super 以后就变得异常简洁了:

let friend = {
  getGreeting() {
    // in the previous example, this is the same as:
    // Object.getPrototypeOf(this).getGreeting.call(this)
    return super.getGreeting() + ", hi!";
  }
};
复制代码

相似其余语言的继承, friend 是实例,它的原型是它的父类,在实例中的 super 实际上是指向父类的引用

所以能够直接在子类中直接使用 super 去使用父类的方法。

只能在简写函数中访问 super

可是 super 只能在对象的简写方法中使用,若是是使用 “function” 关键词声明的函数中使用会出现

syntax error

好比:下面的方式是非法的

let friend = {
  getGreeting: function() {
    // syntax error
    return super.getGreeting() + ", hi!";
  }
};
复制代码

由于 super 在这种函数的上下文中中不存在的。

Object.getPrototypeOf() 并非全部场景都能使用的

由于 this 的指向是根据函数的执行上下文来决定了,所以使用 this 是彻底靠谱的。

好比:

let person = {
  getGreeting() {
    return "Hello";
  }
};

// prototype is person
let friend = {
  getGreeting() {
    return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  }
};
Object.setPrototypeOf(friend, person);


// prototype is friend
let relative = Object.create(friend);

console.log(person.getGreeting());                  // "Hello"
console.log(friend.getGreeting());                  // "Hello, hi!"
console.log(relative.getGreeting());                // error!
复制代码

上面的 relative.getGreeting()) 会报错,缘由是 relative 自己是个新的变量,

这个变量指向由 Object.create(friend) 建立的一个空对象,其原型为 friend

reletive.getGreeting() 的调用首先在 friend 中找但没找到,最后在

friend 中找到了,也就是说它实际上调用的就是原型上的 getGreeting() 而后原型方法里面

又是经过 this 去调用了原型的方法(也就自身),因为 this 始终是根据当前上下文发生变化的,

此时它的指向是 friend ,最终会致使循环调用。

而用 super 就不会有上面的问题,由于 super 指向是固定的,就是指向当前对象的原型对象(父对象),即

这里指向的是 person

super 引用的过程

通常状况下是没什么区别的,可是在咱们作继承或者获取对象的原型的时候就颇有用了,由于 super 的指向是和

[[HomeObject]] 密切相关的, super 获取指向的过程:

  1. 经过在当前方法的内部属性 [[HomeObject]] 上面调用 Object.getPrototypeOf() 去获取这个方法所在对象的原型对象;

  2. 在原型对象上搜与这个函数同名函数;

  3. 最后将这个同名函数绑定当前的 this 执行,而后执行这个函数。

let person = {
  getGreeting() {
    return "Hello";
  }
};

// prototype is person
let friend = {
  getGreeting() {
    return super.getGreeting() + ", hi!";
  }
};
Object.setPrototypeOf(friend, person);

console.log(friend.getGreeting());  // "Hello, hi!"
复制代码

好比,上面的代码

  1. person 设置为 friend 的原型,成为它的父对象

  2. 调用 friend.getGreeting() 执行以后在其内部使用 super.getGreeting() 这个一开始会

找到 friend.getGreeting 这个方法的 [[HomeObject]] 也就是 friend

  1. 而后根据扎到的 friend ,经过 Object.getPrototypeOf() ,去找到原型对象,即 person ,找到以后再去这里面找同名函数 getGreeting

  2. 找到以后将该函数执行上下文绑定到 this (即 friend 所在的上下文)。

  3. 执行同名函数,此时这个虽是原型(person)上的函数,可是上下文已经被绑定到了 friend

过程简单描述就是:

设置继承
=> 重写方法
=> super 调用父级方法
=> 找当前函数的 [[HomeObject]]
=> Object.getPrototypeOf([[HomeObject]]) 找原型
=> 找原型上同名函数
=> 绑定找到的同名函数到当前的 this
=> 执行同名函数

var person = {
  fnName: 'person',
  getName() {
    return this.fnName
  }
}
var child = {
  fnName: 'child',
  getName() {
    return super.getName() + ',' + this.fnName
  }
}

Object.setPrototypeOf(child, person)

console.log(child.getName()) // child child
复制代码

方法定义

在 es6 以前是没有“方法”这个词的定义的,但在 es6 以后对方法的定义才正式有了规定。

函数和方法定义

在对象中的函数才叫作方法,非对象中的叫作函数,且 es6 给方法增长了一个 [[HomeObject]] 内置属性,

它指向的是包含这个方法的那个对象。

好比:

let person = {
  // method
  getGreeting() {
    return 'xxx'
  }
}

// not method
function shareGreeting() {
  return 'yyy'
}
复制代码

getGreeting 叫作方法,且其有个内部属性 [[HomeObject]] 指向了 person 说明这个对象拥有它。

shareGreeting 叫作函数,不是方法

总结

更新内容

内容 示例/说明
属性简写 {name, age} <=> {name: name, age: age}
计算属性 { [first + 'name']: '张三' }, { ['first name']: '张三' }
简写方法 { getName() {} }
重复属性名合法化 { age: 10, age: 100 } <=> { age: 100 }
Object.assign 合并对象 浅拷贝,内部 = 实现拷贝
Object.is 增强判断,弥补 === 不能判断 +0, -0NaN, NaN 问题
固定对象属性枚举顺序 number > string > symbol, string 和 symbol 按照增长前后顺序排列
Object.setPrototypeOf 可改变对象原型
super 指向原型对象,可经过它去访问原型对象中的方法

数据解构

解构优点

在 es5 及以前若是咱们想要从对象中取出属性的值,只能经过普通的赋值表达式来实现,

一个还好,若是是多个的话就会出现很重复的代码,好比:

let options = {
  repeat: true,
  save: false
}

let repeat = options.repeat,
    save = options.save


// if more ???
复制代码

上面只是取两个对象的属性,若是不少呢,十几个二十几个??

不只代码量大,还不美观。

所以 es6 加入了解构系统,让这些操做变的很容易,很简洁。

对象解构

对象解构的时候,等号右边不能是 nullundefined ,这样会报错,这是由于,不管何时

去读取 nullundefined 的属性都会出发运行时错误。

声明式解构

解构的同时声明解构后赋值的变量:

let node = {
  type: 'Identifier',
  name: 'foo'
}

let { type, name } = node

console.log(type) // Identifier
console.log(name) // foo
复制代码

在使用解构的过程当中必需要有右边的初始值,而不能只是用来声明变量,这是不合法的操做

好比:

// syntax error!
var { type, name };

// syntax error!
let { type, name };

// syntax error!
const { type, name };
复制代码

先声明后解构

有时候有些变量早已经存在了,只是后面咱们须要将它的值改变,也正好是须要从对象中去取值,

这个时候就是先声明后解构:

let node = {
  type: "Identifier",
  name: "foo"
},
    // 这里变量已经声明好了
    type = "Literal",
    name = 5;

// assign different values using destructuring
({ type, name } = node);

console.log(type);      // "Identifier"
console.log(name);      // "foo"
复制代码

这个时候必须用 () 将解构语句包起来,让其成为一个执行语句,若是不,左边就至关于

一个块级语句,然而块级语句是不能出如今等式的左边的。

在这基础上,另外一种状况是将 {type, name} = node 做为参数传递给函数的时候,这个时候

传递给函数的参数其实就是 node 自己,例如:

let node = {
  type: "Identifier",
  name: "foo"
},
    type = "Literal",
    name = 5;

function outputInfo(value) {
  console.log(value === node);
}

outputInfo({ type, name } = node);        // true

console.log(type);      // "Identifier"
console.log(name);      // "foo"
复制代码

解构默认值

在解构过程当中,可能左边声明的变量在右边的对象中并不存在或者值为 undefined 的时候,这个变量的值将会

赋值为 undefined ,所以这个时候就须要针对这种状况有个默认处理,即这里的解构默认值。

let node = {
  type: "Identifier",
  name: "foo"
};

let { type, name, value } = node;

console.log(type);      // "Identifier"
console.log(name);      // "foo"
console.log(value);     // undefined
复制代码

属性值为 undefined 的状况:

let node = {
  type: "Identifier",
  name: "foo",
  value: undefined
};

let { type, name, value = 0 } = node;

console.log(type);      // "Identifier"
console.log(name);      // "foo"
console.log(value);     // 0
复制代码

属性变量重命名

解构出来以后,可能不想沿用右边对象中的属性名,所以须要将左边的变量名称重命名:

let node = {
  type: "Identifier",
  name: "foo"
};

let { type: localType, name: localName } = node;

console.log(localType);     // "Identifier"
console.log(localName);     // "foo"
复制代码

重命名 + 默认值:

let node = {
  type: "Identifier",
  name: "foo"
};

let { type: localType, name: localName = 'xxx' } = node;

console.log(localType);     // "Identifier"
console.log(localName);     // "foo"
复制代码

多级对象解构

右边对象中的属性的值不必定是普通类型,多是对象,或对象中包含对象,数组等等类型,次数能够

使用内嵌对象解构来进行解构:

原则就是左边的变量的结构要和右边实际对象中的结构保持一致

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1
    },
    end: {
      line: 1,
      column: 4
    }
  }
};

let { loc: { start }} = node;

console.log(start.line);        // 1
console.log(start.column);      // 1
复制代码

多层解构重命名:

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1
    },
    end: {
      line: 1,
      column: 4
    }
  }
};

// 重命名
let { loc: { start: localStart }} = node;

console.log(start.line);        // 1
console.log(start.column);      // 1

复制代码

{% note info %}

语法陷阱

// no variables declared!
let { loc: {} } = node;
复制代码

这种形式其实是没任何做用的,由于左边的 loc 只是起到了站位的做用,实际起做用的

是在 {} 里面,可是里面没任何东西,也就是说这个不会解构出任何东西,也不会产生任何新的变量。

{% endnote %}

数组解构

数组解构和对象解构用法基本是同样的,无非就是讲 {} 改为数组的 [] ,和对象同样,右边不能够是 nullundefined

表达式 结果 说明
let [first, second] = [1, 2] first = 1, first = 2 普通解构
let [ , , third] = [1, 2, 3] third = 3 空置解构,只指定某个位置解构
let first = 1, second = 2 => [first, second] = [11, 22] first = 11, second = 22 先声明再解构
let a = 1, b = 2 => [a, b] = [b, a] a = 2, b = 1 替换值快捷方式
let [a = 1, b] = [11, 22] a = 11, b = 22 默认值
let [a = 1, b] = [, 22] a = 1, b = 22 默认值
let [a, b = 2] = [ 1 ] a = 1, b = 2 默认值
let [a, [b]] = [1, [2]] a = 1, b = 2 嵌套解构
let [a, [b]] = [1, [2, 3], 4] a = 1, b = 2 嵌套解构
let [a, [b], c] = [1, [2, 3], 4] a = 1, b = 2, c = 4 复杂解构
let [a, ...bs] = [1, 2, 3, 4, 5] a = 1, bs = [2, 3, 4, 5] rest 符号解构
[1, 2, 3].concat() => [1, 2, 3] => es6: [...as] = [1, 2, 3] as = [1, 2, 3] 克隆数组

混合解构

混合解构意味着被解构的对象中可能既包含对象由包含数组,也是按照对象和数组的解构原理进行解构就OK。

let node = {
  type: "Identifier",
  name: "foo",
  loc: {
    start: {
      line: 1,
      column: 1
    },
    end: {
      line: 1,
      column: 4
    }
  },
  range: [0, 3]
};

let {
  loc: { start },
  range: [ startIndex ]
} = node;

console.log(start.line);        // 1
console.log(start.column);      // 1
console.log(startIndex);        // 0
复制代码

参数解构

参数解构,即函数在声明的时候,参数是采用解构等式左边的形式书写,这种就须要要求在调用的时候

这个参数位置必须有个非 null 和 Undefined 值,不然会报错,缘由同样解构时候没法从 null 或 undefined 读取属性。

被解构的参数属性列表

实例:

function setCookie(name, value, { secure, path, domain, expires }) {

  // code to set the cookie
}

setCookie("type", "js", {
  secure: true,
  expires: 60000
})
复制代码

不传值得非法操做:

// Error!
setCookie("type", "js");
复制代码

这样第三个参数就是 undefined 报错。

优化参数解构写法有两种:

  1. 函数体内解构
  2. 解构体默认值方式(推荐)

函数体内解构:

function setCookie(name, value, options) {

  // 函数体内解构,给个默认值 || {} ,或者在参数那里这样: (name, value, options = {})
  let { secure, path, domain, expires } = options || {};

  // code to set the cookie
}
复制代码

或者:

function setCookie(name, value, options = {}) {

  let { secure, path, domain, expires } = options;

  // code to set the cookie
}

复制代码

直接参数解构体给默认值:

function setCookie(name, value, { secure, path, domain, expires } = {}) {

  // ...
}
复制代码

默认值,若是不传第三个参数,那么它的默认值就是 {} 避免解构出错。

解构的参数默认值

和普通对象同样,解构出来的参数咱们还能够给他们一个默认值:

function setCookie(name, value, { secure = false, path = "/", domain = "example.com", expires = new Date(Date.now() + 360000000) } = {}
                  ) {

  // ...
}
复制代码
  1. 第三个参数没传,四个参数都取默认值
  2. 第三个参数有传递,根据普通对象定义解构

总结

  1. 对象,先声明再解构,表达式必须用 () 包起来,做为表达式执行
  2. 对象数组解构均可以给默认值,重命名,多层解构,混合解构
  3. 解构遵循左侧最内层的变量声明,若是左侧最内层无任何变量,则解构表达式无任何意义
  4. 参数解构,要么给当前参数默认值,要么保证调用时该参数都有传入非 nullundefined 的值,推荐参数默认值

符号和符号属性(Symbols)

符号类型值(Symbol())是 es6 新增的一种原始数据类型和 strings, numbers, booleans, nullundefined 属于原始值类型。

它至关于数字的 42 或字符串的 "hello" 同样,只是单穿的一些值,所以不能对其使用 new Symbol() 不然会报错。

img

符号类型是做为一种建立私有对象成员的类型,在 es6 以前是没有什么方法能够区分普通属性和私有属性的。

建立符号

符号类型会建立一个包含惟一值得符号变量,这些变量是没有实际字面量表示的,也就是说一旦符号变量建立以后,只能经过这个变量

去访问你所建立的这个符号类型。

建立符号

经过 Symbol([ description ]) 来建立符号,建立过程:

  1. 若是 descriptionundefined, 让 descString = undefined
  2. 不然 descString = ToString(description)
  3. 让内部值 [[Description]]descString
  4. 返回一个惟一的 Symbol 值
let firstName = Symbol();
let secondName = Symbol();
let person = {};

person[firstName] = "Nicholas";
console.log(person[firstName]);     // "Nicholas"

console.log(firstName)
console.log(secondName)
console.log(firstName == secondName)
console.log(firstName === secondName)
console.log(Object.is(firstName, secondName))
复制代码

+RESULTS:

Nicholas
Symbol()
Symbol()
false
false
false
复制代码

firstName 是存放了一个惟一值得符号类型变量,而且用来做为 person 对象的一个属性使用。

所以,若是要访问对象中的对应的这个属性的值,每次都必须使用 firstName 这个符号变量去访问。

{% note info %}
若是须要实在须要符号类型对象,能够经过 new Object(Symbol()) 去建立一个对象,而不能

直接 new Symbol() 由于 Symbol() 获得的是一个原始值,就像你不能直接 new 42 一个道理。

img

{% endnote %}

带参数的 Symbol(arg)

有时候可能须要对建立的符号作一些简单的区分,或者让其更加语义化,能够在建立的时候给 Symbol() 函数

一个参数,参数自己并无实际的用途,可是有利于代码调试。

let firstName = Symbol("first name");
let person = {};

person[firstName] = "Nicholas";

console.log("first name" in person);        // false
console.log(person[firstName]);             // "Nicholas"
console.log(firstName);                     // "Symbol(first name)"
console.log(firstName.description) // undefined
console.log(Symbol('xxx').description) // undefined
复制代码

+RESULTS:

false
Nicholas
Symbol(first name)
undefined
undefined
复制代码

如输出,参数会一并输出,所以推荐使用的时候加上参数,这样在调试的时候你就能区分开哪一个符号

来自哪里,而不至于输出都是 Symbol() 没法区分。

参数做为符号的一种描述性质特征被储存在了内部 [[Description]] 属性中,这个属性会在对符号调用 toString()

(隐式或显示调用)的时候去读取它的值,除了这个没有其余方法能够直接去访问 [[Description]]

符号类型检测(typeof)

因为符号属于原始值,所以能够直接经过 typeof 就能够去判断变量是否是符号类型,es6 对 typeof 进行了扩展,

若是是符号类型检测的结果值是“symbol”

let symbol = Symbol("test symbol")

console.log(typeof symbol) // "symbol"
复制代码

+RESULTS:

symbol
复制代码

使用符号

以前的例子中使用变量做为对象属性名的,均可以使用符号来替代,而且还能够对符号类型的属性

进行定制,让其变成只读的。

// 建立符号,惟一
let firstName = Symbol('first name')

let person = {
  // 直接当作计算属性使用
  [firstName]: '张三'
}

// 让属性只读
Object.defineProperty(person, firstName, { writable: false })

let lastName = Symbol('last name')

Object.defineProperties(person, {
  [lastName]: {
    value: '李四',
    writable: false
  }
})

console.log(person[firstName])
console.log(person[lastName])
复制代码

+RESULTS:

张三
李四
复制代码

分享符号

在使用过程当中咱们须要考虑一个问题:

假设某个地方声明了一个符号类型及一个使用了这个符号做为属性 key 的对象,哪天

若是我想在其余地方去使用它,该怎么办??

现在模块化获得普及,如今常常都是一个文件一个模块,用的时候导入这个文件获得相应的对象

但因为符号值是惟一的,那外部模块又怎么知道另外一个模块内部用了怎样的符号值做为对象??

这就是下面要讲的“符号分享”问题。

{% note warn %}
全局符号注册表(Global Symbol Registry) 会在全部代码执行以前就建立好,且列表为空。

它和全局对象同样属于环境变量,所以不要去假设它是什么或它不存在之类的,所以它在全部代码执行以前

就建立好了,因此它是确确实实存在的。
{% endnote %}

Symbol.for()

在以前咱们经过 let firstName = Symbol('first name'); 来建立一个符号变量,可是在使用的时候必须的用

firstName 去使用这个变量,而如今咱们想将符号分享出去须要用到 Symbol.for()

Symbol.for(description) 会针对 description 去建立一个惟一的符号值:

let uid = Symbol.for("uid");
let object = {};

object[uid] = "12345";

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"
复制代码

Symbol.for(desc) 在第一次调用的时候,首先会去“全局符号注册表(global symbol registry)” 中去查找

这个 desc 对应的符号值,找到了就返回这个符号值,若是没找到会建立一个新的符号值而且将它注册到全局符号注册表中,

供下次调用时使用。

-—

Symbol.for(key) 内部实现步骤(伪代码):

Symbol.for = function (key) {

  // 1 key 转字符串
  let stringKey = ToString(key);

  // 2. 遍历 GlobalSymbolRegistryList 注册表
  for (let e in GlobalSymbolRegistryList) {
    // 符号值已经存在
    if (SameValue(e.[[Key]], stringKey)) {
      return e.[[Symbol]];
    }
  }

  // 3. 注册表中不含 `stringKey` 的符号值,则建立新的符号值
  // 3.1 新建符号值
  let newSymbol = Symbol(stringKey);
  // 3.1 给 [[Description]] 赋值
  newSymbol.[[Description]] = stringKey;

  // 4. 注册到符号注册表中去
  GlobalSymbolRegistryList.push({
    [[Key]]: stringKey,
    [[Symbol]]: newSymbol
  });

  // 5. 返回新建的符号值
  return newSymbol;

}
复制代码

总结起来为3个步骤: 查找 -> 新建 -> 注册

注册表中的每一个符号片断是以对象形式存在(对象中包含 KeySymbol 两个属性分别表示建立时的描述和符号值)。

使用分享符号

在上一节7.3.1 中咱们描述过了用来建立分享符号的 Symbol.for(desc) 接口,这里将探讨如何具体使用它来分享符号值。

let uid = Symbol.for("uid");
let object = {
  [uid]: "12345"
};

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"

let uid2 = Symbol.for("uid");

console.log(uid === uid2);      // true
console.log(object[uid2]);      // "12345"
console.log(uid2);              // "Symbol(uid)
复制代码

在当前代码运行的全局做用域中均可以分享到一份 Symbol.for("uid") 符号,只须要调用它就能够拿到那个

惟一的值。

好比:

function createObj1() {
  let uid = Symbol.for("uid");
  let object = {
    [uid]: "12345"
  };

  return object
}

function createObj2() {
  let uid = Symbol.for("uid");
  let object = {
    [uid]: "67890"
  };

  return object
}


let uid1 = Symbol.for("uid");
const obj1 = createObj1()

let uid2 = Symbol.for("uid");
const obj2 = createObj2()

console.log(uid1 === uid2);
console.log(obj1[uid1]);
console.log(obj1[uid2]);
console.log(obj2[uid1]);
console.log(obj2[uid2]);

复制代码

+RESULTS:

true
12345
12345
67890
67890
复制代码

Symbol.keyFor(symbolValue)

咱们若是想建立或获取全局注册表中的符号是能够经过 7.3.1 中的 Symbol.for(key) ,可是

若是咱们只知道一个符号值变量的状况下,使用 Symbol.for(key) 就无法从注册表中取值了。

所以,这里将介绍如何使用 Symbol.keyFor(symbolValue) 去根据符号变量查找注册表中的值。

在这以前须要知道

  1. Symbol.for(key) 建立的符号才会进入全局注册表
  2. Symbol() 直接建立的是不会加入全局注册表的

也就有了下面的代码及结果:

let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid));    // "uid"

let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2));   // "uid"

let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3));   // undefined
复制代码

+RESULTS:

uid
uid
undefined
复制代码

所以 Symbol("uid"); 结果不会加入注册表,所以结果是 undefined

符号强制转换

在 JavaScript 中类型强制转换是常常会被用到的一个特性,也让 JavaScript 使用起来会很灵活地能够将一个

数据类型转成另外一种数据类型。

可是符号类型不支持强制转换。

let uid = Symbol.for("uid")

console.log(uid) // Symbol(uid)

// 在输出的时候其实是调用了 uid.toString()
复制代码

+RESULTS:

Symbol(uid)
复制代码

当咱们将符号变量加入计算或字符串操做时会报错,由于两个不一样类型的值进行操做会发生隐式转换,可是符号类型不支持强转

的,所以会报异常。

let uid = Symbol.for('uid'),
    desc = '',
    sum = 0

try {
  desc = uid + ""
} catch (e) {
  console.log(e.message)
}

try {
  sum = uid / 1
} catch (e) {
  console.log(e.message)
}
复制代码

+RESULTS: 异常信息

Cannot convert a Symbol value to a string
Cannot convert a Symbol value to a number
复制代码

获取对象符号属性

获取对象属性的方法:

  1. Object.keys() 会获取全部可枚举的属性
  2. Object.getOwnPropertyNames() 获取全部属性,忽略可枚举性

可是为了兼容 es5 及之前的版本,他们都不会去获取符号属性,所以须要使用 Object.getOwnPropertySymbols()

去单独获取对象全部的符号属性,返回一个包含全部符号属性的数组。

let uid = Symbol.for("uid");
let object = {
  [uid]: "12345",
  [Symbol.for("uid2")]: "67890"
};

let symbols = Object.getOwnPropertySymbols(object);

console.log(symbols.length);        // 1
console.log(symbols[0]);            // "Symbol(uid)"
console.log(object[symbols[0]]);    // "12345"
复制代码

+RESULTS:

2
Symbol(uid)
12345
复制代码

符号内部操做(方法)

在 es6 中 JavaScript 的许多特性中其内部的实现都是使用到了符号内部方法。

好比下表涉及到的内容

符号方法 类型 JavaScript 特性 描述
Symbol.hasInstance boolean instanceof 7.6.1 实例(原型链)检测
Symbol.isConcatSpreadable boolean Array.prototype.concat 7.6.2 检测参数合法性
Symbol.iterator function 调用后获得迭代器 遍历对象或数组(等可迭代的对象)的时候会用到
Symbol.asyncIterator function 调用后获得异步迭代器(返回一个 Promise ) 遍历对象或数组(等可迭代的对象)的时候会用到
Symbol.match function String.prototype.match 7.6.3 正则表达式对象内部属性
Symbol.matchAll function String.prototype.matchAll 7.6.3 正则表达式对象内部属性
Symbol.replace function String.prototype.replace 7.6.3 正则表达式对象内部属性
Symbol.search function String.prototype.search 7.6.3 正则表达式对象内部属性
Symbol.split function String.prototype.split 7.6.3 正则表达式对象内部属性
Symbol.species constructor - 派生对象生成
Symbol.toPrimitive function - 7.6.4 返回一个对象的原始值
Symbol.toStringTag string Object.prototype.toString() 7.6.5 返回一个对象的字符串描述
Symbol.unscopables object with 7.6.8 不能出如今 with 语句中的一个对象

{% note info %}
经过改变对象的上面的内部符号属性的实现,可让咱们去修改对象的一些

默认行为,好比 instanceof 一个对象的时候能够改变它的行为让它返回一个非预期值。
{% endnote %}

Symbol.hasInstance

每一个函数都有一个内部 Symbol.hasInstance 方法用来判断给定的对象是否是这个函数的一个实例。

这个函数定义在 Function.prototype 上,所以全部的函数都会继承 instanceof 属性的默认行为,

而且这个方法是 nonwritable, nonconfigurable, 和 nonenumerable 的,确保它不会被错误的

重写。

所以下面的中的两句 obj instanceof ArrayArray[Symbol.hasInstance](obj) 是等价的。

const obj = {}

let v1 = obj instanceof Array;

// 等价于

let v2 = Array[Symbol.hasInstance](obj);

console.log(v1, v2)
复制代码

+RESULTS:

false false
复制代码

在 es6 中实际上已经对 instanceof 操做作了重定义,其内部还让它支持了函数调用方式,即

其内部的 Symbol.hasInstance 再也不限定只是 boolean 类型,它还能够是函数类型,所以

咱们能够经过重写这个方法来改变 instanceof 的默认行为。

好比:让一个对象的 instanceof 操做老是返回 false

function MyObj() {
  // ...
}

Object.defineProperty(MyObj, Symbol.hasInstance, {
  value: function(v) {
    console.log('override method')
    return false;
  }
})

let obj = new MyObj();

console.log(obj instanceof MyObj); // false
复制代码

+RESULTS:

override method
false
复制代码

因为 Symbol.hasInstance 属性是 nonwritable 的所以须要经过 Object.defineProperty

去从新定义这个属性。

{% note warn %}
虽然 es6 赋予了这种能够重写一些 JavaScript 特性的默认行为的能力,可是依旧不推荐

去这么作,极可能让你的代码变得很不可控,也不容易让人理解你的代码。
{% endnote %}

Symbol.isConcatSpreadable

对应着 Array.prototype.concat 的内部使用 Symbol.isConcatSpreadable

concat 使用示例:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat([ "blue", "black" ]);

console.log(colors2.length);    // 4
console.log(colors2);           // ["red","green","blue","black"]
复制代码

+RESULTS:

4
[ 'red', 'green', 'blue', 'black' ]
复制代码

咱们通常用 concat 去扩展一个数组,把他们合并到一个新的数组中去。

根据 Array.prototype.concat(value1, ...valueNs) 的定义,它是能够接受 n 多个参数的,好比:

[].concat(1, 2, 3, ...) > =[1, 2, 3, ...]

而且并无限定参数的类型,即这些 value1, ...valuesNs 能够是任意类型的值(数组,对象,纯值等等)。

另外,若是参数是数组的话,它会将数组项一一展开合并到源数组中区(且只会作一级展开,数组中的数组不会展开)。

好比:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat(
      [ "blue", "black", [ "white" ] ], "brown", { color: "red" });

console.log(colors1 === colors2)
console.log(colors2.length);    // 5
console.log(colors2);           // ["red","green","blue","black","brown"]
复制代码

+RESULTS:

false
7
[ 'red',
  'green',
  'blue',
  'black',
  [ 'white' ],
  'brown',
  { color: 'red' } ]
复制代码

可是,若是咱们须要的是将 { color: 'red' } 中的属性值 'red' 合并到数组末尾,该如何作??

->>> Symbol.isConcatSpreadable 就是它

和其余内置符号不同,这个在全部的对象中默认是不存在的,所以若是咱们须要就得手动去添加,让这个对象

变成 concatable 只须要将这个属性值置为 true 便可:

let collection = {
  0: 'aaa',
  '1': 'bbb',
  length: 2,
  [Symbol.isConcatSpreadable]: true
}

let objNoLength = {
  0: 'xxx',
  1: 'yyy',
  [Symbol.isConcatSpreadable]: true
}


let objNoNumberAttrs = {
  a: 'www',
  b: 'vvv',
  length: 2,
  [Symbol.isConcatSpreadable]: true
}

let words = [ 'somthing' ];

console.log(words.concat(collection).toString())
console.log(words.concat(objNoLength).toString())
console.log(words.concat(objNoNumberAttrs).toString())
复制代码

+RESULTS:

somthing,aaa,bbb
somthing
somthing,,
复制代码

分析结果得出,对象要变的能够被 Array.prototype.concat 使用,

须要知足如下条件:

  1. 必须有 length 属性,不然对结果没任何影响,如结果第二行输出: somthing
  2. 必须有以数字为 key 的属性,不然数组中将使用空值代替追加的值追加到数组中去,如第三行输出: somthing,,
  3. 必须增长符号属性 Symbol.isConcatSpreadable 且值为 true

同理,咱们能够将数组对象的 Symbol.isConcatSpreadable 符号属性置为 false 来阻止数组的 concatable 行为。

Symbol.match, Symbol.replace, Symbol.search, Symbol.split

和字符串,正则表达式有关的一些符号,对应着字符串和正则表达式的方法:

  • match(regex) 字符串是否匹配正则
  • replace(regex, replacement) 字符串替换
  • search(regex) 字符串搜索
  • split(regex) 字符串切割

这些都须要用到正则表达式 regex

在 es6 以前这些方法与正则表达式的交互过程对于开发者而已都是隐藏了其内部细节的,也就是

说开发者没法经过本身定义的对象去表示一个正则。

在 es6 中定义了四个符号即是用来实现 RegExp 内部实现对象,便可以经过对象的方式去

实现一个正则表达式规则。

这四个符号属性是在 RegExp.prototype 原型上被定义的,做为以上方法的默认实现。

{% note info %}
意思就是 math, replace, search, split 这四个方法的 regex 正则

表达式的内部实现基于对应的四个符号属性函数 Symbol.math, Symbol.replace,

Symbol.search, Symbol.split
{% endnote %}

  • Symbol.match 接受一个字符串参数,若是匹配会返回一个匹配的数组,未匹配返回 null
  • Symbol.replace 接受一个字符串参数和一个用来替换的字符串,返回一个新的字符串。
  • Symbol.search 接受一个字符串,返回匹配到的数字因此呢,未匹配返回 -1。
  • Symbol.split 接受一个字符串,返回以匹配到的字符串位置分割成的一个字符串数组
// 等价于 /^.${10}$/
let hasLengthOf10 = {
  [Symbol.match]: function(value) {
    return value.length === 10 ? [value] : null
  },

  [Symbol.replace]: function(value, replacement) {
    return value.length === 10 ? replacement : value
  },

  [Symbol.search]: function(value) {
    return value.length === 10 ? 0 : -1
  },

  [Symbol.split]: function(value) {
    return value.length === 10 ? ["", ""] : [value]
  }
}

let msg1 = "Hello World", // 11 chars
    msg2 = "Hello John"; // 10 chars


let m1 = msg1.match(hasLengthOf10)
let m2 = msg2.match(hasLengthOf10)

console.log(m1)
console.log(m2)

let r1 = msg1.replace(hasLengthOf10, "Howdy!")
let r2 = msg2.replace(hasLengthOf10, "Howdy!")

console.log(r1)
console.log(r2)


let s1 = msg1.search(hasLengthOf10)
let s2 = msg2.search(hasLengthOf10)

console.log(s1)
console.log(s2)

let sp1 = msg1.split(hasLengthOf10)
let sp2 = msg2.split(hasLengthOf10)

console.log(sp1)
console.log(sp2)
复制代码

+RESULTS:

null
[ 'Hello John' ]
Hello World
Howdy!
-1
0
[ 'Hello World' ]
[ '', '' ]
复制代码

经过这几个正则对象的内部符号属性,使得咱们有能力根据须要去完成更复杂的正则匹配规则。

Symbol.toPrimitive

在 es6 以前,若是咱们要使用 == 去比较两个对象的时候,其内部都会讲对象转成原始值以后再去比较,

且此时的转换属于内部操做,咱们是没法知晓更没法干涉的。

但在 es6 出现以后,这种内部实现经过 Symbol.toPrimitvie 被暴露出来了,从而使得咱们有能力取

改变他们的默认行为。

Symbol.toPrimitvie 是定义在全部的标准类型对象的原型之上,用来描述在对象被转换成原始值以前的

都作了些什么行为。

当一个对象发生原始值转换的时候, Symbol.toPrimitive 就会带上一个参数(hint)被调用,这个参数值为

"number", "string", "default" 中的一个(值是由 JavaScript 引擎所决定的),分别表示:

  1. "number" :表示 Symbol.toPrimitive 应该返回一个数字。
  2. "string" :表示 Symbol.toPrimitvie 应该返回一个字符串。
  3. "default" : 表示原样返回。

在大部分的标准对象中, number 模式的行为按照如下的优先级来返回:

  1. 先调用 valueOf() 若是结果是一个原始值,返回它。
  2. 而后调用 toString() 若是结果是一个原始值,返回它。
  3. 不然,抛出异常。

一样, string 模式的行为优先级以下:

  1. 先调用 toString() 若是结果是一个原始值,返回它。
  2. 而后调用 valueOf() 若是结果是一个原始值,返回它。
  3. 不然,抛出异常。

在此,能够经过重写 Symbol.toPrimitive 方法,能够改变以上的默认行为。

{% note info %}
"default" 模式仅在使用 ==, + 操做符,以及调用 Date 构造函数的时候

只传递一个参数的时候才会用到。大部分的操做都是采用的 "number" 或 "string" 模式。
{% endnote %}

实例:

function Temperature(degrees) {
  this.degrees = degrees
}

let freezing = new Temperature(32)

console.log(freezing + "!") // [object Object]!
console.log(freezing / 2) // NaN
console.log(String(freezing)) // [object Object]
复制代码

输出结果:

img

由于默认状况下一个对象字符串化以后会变成 [object Object] 这是其内部的默认行为。

经过重写原型上的 Symbol.toPrimitive 函数能够改写这种默认行为。

好比:

function Temperature(degrees) {
  this.degrees = degrees
}

Temperature.prototype[Symbol.toPrimitive] = function(hint) {
  switch (hint) {
  case 'string':
    return this.degrees + '\u00b0'
  case 'number':
    return this.degrees
  case 'default':
    return this.degrees + " degrees"
  }
}

let freezing = new Temperature(32)

console.log(freezing + "!")
console.log(freezing / 2)
console.log(String(freezing))
复制代码

+RESULTS:

32 degrees!
16
32°
复制代码

结果就像咱们以前分析的, 只有 ==+ 执行的是 “default" 模式,

其余状况执行的要么是 "number" 模式(如: freezing / 2)

要么是 "string" 模式(如: String(freezing))

Symbol.toStringTag 介绍

在 JavaScript 的一个有趣的问题是,能同时拥有多个全局执行上下文的能力。

这个发生在 web 浏览器环境下,一个页面可能包含一个 iframe ,所以当前页面和这个 iframe 各自

都拥有本身的执行环节。

一般状况下,这并非什么问题,由于数据能够经过一些手段让其它当前页和 iframe 之间进行传递,

问题是如何去识别这个被传递的对象是源自哪一个执行环境??

好比,一个典型的问题是在 pageiframe 之间互相传递一个数组。在 es6 的术语中, 页面和

iframe 每个都表明着一个不一样的领域(realm, JavaScript 执行环境)。每一个领域都有它本身的全局

做用域包含了它本身的一份全局对象的副本。

不管,数组在哪一个领域被建立,它都很明确的是一个数组对象,当它被传递到另外一个领域的时候,使用

instanceof Array 的结果都是 false ,由于数组是经过构造函数在别的领域所建立的,而

Array 表明的仅仅是当前领域下的构造函数,即两个领域下的 Array 不是一回事。

这就形成了在当前领域下去判断另外一个领域下的一个数组变量是否是数组,获得的结果将是 false

Symbol.toStringTag 延伸(不一样 realm 下的对象识别)

对象识别的应对之策(Object.prototype.toString.call(obj))

function isArray(value) {
  return Object.prototype.toString.call(value) === "[object Array]";
}

console.log(isArray([]));   // true
复制代码

+RESULTS:

true
复制代码

这种方式虽然比较麻烦,可是倒是最靠谱的方法。

由于每一个类型的 toString() 可能有本身的实现,返回的值是没法统一的,可是 Object.prototype.toString

返回的内容始终是 [object Array] 这种,后面是被检测数据表明的类型的构造函数,它老是能获得正确且精确的

结果。

Object.prototype.toString 内部实现的伪代码:

// toString(object)

function toString(obj) {
  // 1. 判断 undefined 和 null
  if (this === undefined) {
    return '[object Undefined]';
  }

  if (this === null) {
    return '[object Null]';
  }

  let O = ToObject(this); // 上下文变量对象化
  let isArray = IsArray(O); // 先判断是否是数组类型
  let builtinTag = ''

  let has = builtinName => !!O.builtinName;

  // 2. 根据内置属性,检测各对象的类型
  if (isArray === true) { // 数组类型
    builtinTag = 'Array';
  } else if ( has([[ParameterMap]]) ) { // 参数列表,函数参数对象
    // 函数的参数 arguments 对象
    builtinTag = 'Arguments';
  } else if ( has([[Call]]) ) { // 函数
    builtinTag = 'Function';
  } else if ( has([[ErrorData]]) ) { // Error对象
    builtinTag = 'Error';
  } else if ( has([[BooleanData]]) ) { // Boolean 布尔对象
    builtinTag = 'Boolean';
  } else if ( has([[StringData]]) ) { // String 对象
    builtinTag = 'String';
  } else if ( has([[DateValue]]) ) { // Date 对象
    builtinTag = 'Date';
  } else if ( has([[RegExpMatcher]]) ) { // RegExp 正则对象
    builtinTag = 'RegExp';
  } else {
    builtinTag = 'Object' // 其余
  }

  // 3. 最后检测 @@toStringTag - Symbol.toStringTag 的值
  let tag = Get(O, @@toStringTag);

  if (Type(tag) !== 'string') {
    tag = builtinTag;
  }

  return `[object ${tag}]`;
}

复制代码

从伪代码中咱们知道,最后的实现中使用到了 @@toStringTag 即对应这里的 Symbol.toStringTag 属性值,

而且这个放在最后判断,优先级最高,即若是咱们重写了 Symbol.toStringTag 那么重写以后的返回值将

最优先返回。

Symbol.toStringTag 的 ES6 实现

正如 7.6.6 中的伪代码所示,在 es6 中对于 Object.prototype.toString.call(obj)

的实现中加入了 @@toStringTag 内部属性的检测,即对应着这里的 Symbol.toStringTag ,那么咱们便

能够经过改变这个值来修改它的默认行为,从而获得咱们想要的类型值。

好比:咱们有一个 Person 构造函数,咱们但愿在使用 toString() 的时候获得结果是 [object Person]

function Person(name) {
  this.name = name
}

Person.prototype[Symbol.toStringTag] = 'Person'

let me = new Person('xxx')

Person.prototype.toString = () => '[object Test]'

console.log(me.toString()) // [object Person]
console.log(Object.prototype.toString.call(me)) // [object Person]
console.log(me.toString === Object.prototype.toString) // true

复制代码

+RESULTS: 未重写 Person.prototype.toString 结果

[object Person]
[object Person]
true
复制代码

+RESULTS: 重写 Person.prototype.toString 的结果

[object Test]
[object Person]
false
复制代码

咱们发现就算重写了 Person.prototype.toString 也不会影响 Symbol.toStringTag 赋值后的运行结果,

如后面调用 Object.prototype.toString.call(me) 结果依旧是 [object Person]

由于咱们重写了 Symbol.toStringTag 属性值,所以7.6.6实现部分:

// 3. 最后检测 @@toStringTag - Symbol.toStringTag 的值
let tag = Get(O, @@toStringTag); // 这里的结果就成了 'Person'

if (Type(tag) !== 'string') {
  tag = builtinTag;
}

return `[object ${tag}]`
复制代码

所以获得 [object Person] 返回结果。

咱们还能够经过重写 Person 自身的 toString() 的实现让其拥有本身的默认行为,上面的第三行

结果代表 me.toString() 最终调用的是 Object.prototype.toString

Symbol.unscopables

with 语句在 JavaScript 世界中是最具争议的一项特性之一。

本来设计的初衷是避免重复书写同样的代码,可是在实际使用过程当中,倒是让代码更难理解,很容易出错,

也有性能上的影响。

虽然,极力不推荐使用它,可是在 es6 中为了考虑向后兼容性问题,在非严格模式下依旧对它作了支持。

好比:

let values = [1, 2, 3],
    colors = ["red", "green"],
    color = "black";

with(colors) {
  push(color);
  push(...values);
}

console.log(colors.toString())
复制代码

+RESULTS:

red,green,black,1,2,3
复制代码

上面代码,在 with 里面调用的两次 push 等价于 colors.push 调用,

由于 with 将本地执行上下文绑定到了 colors 上。

values, color 指向的均是在 with 语句外面建立的 valuescolor

可是在 ES6 中给数组增长了一个 values 方法,这个方法会返回当前数组的迭代器对象: Array Iterator {}

这就意味着在 ES6 的环境中, values 指向的将是数组自己的 values() 方法而不是外面声明的

values = [1, 2, 3] 这个数组,将破坏整个代码的运行。

这就是 Symbol.unscopables 存在的缘由。

Symbol.unscopables 被用在 Array.prototype 上用来指定那些属性不能在 with 中建立绑定:

// built into ECMAScript 6 by default
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
  copyWithin: true,
  entries: true,
  fill: true,
  find: true,
  findIndex: true,
  keys: true,
  values: true
});

复制代码

上面是默认状况下 ES6 内置的设定,即数组中的上列属性不容许在 with 中建立绑定,从列表能发现这些被

置为 true 的属性都是 es6 中新赠的方法,这主要是为了兼容之前的代码只针对新增的属性这么使用。

{% note warn %}
通常状况下,不须要从新定义 Symbol.unscopables ,除非代码中存在 with 语句而且

须要作一些特殊处理的时候,可是建议尽可能避免使用 with
{% endnote %}

总结

  1. Symbols 是一种新的原始值类型,用来建立一些属性,这些属性只能使用对应的符号或符号变量去访问。
  2. Symbol([description]) 用来建立一个符号,推荐传入描述,便于识别。
  3. Symbol.for(key) 首先查找注册表(GSR),若是 key 对应的符号存在直接返回,若是不存在则建立新符号并加入到注册表,而后返回新建立的符号。
  4. Symbol.keyFor(symbolValue) 经过符号变量从注册表中找到对应的符号值,没有返回 undefined
  5. 符号共享经过 Symbol.for(key)Symbol.keyFor(symbolValue) 可让符号达到共享的目的,由于全局注册表在全部代码运行以前就已经建立好了。
  6. 符号不容许类型转换(或隐式转换)。
  7. Object.keys()Object.getOwnPropertyNames() 不能获取到符号属性。
  8. Object.getOwnPropertySymbols(obj) 能获取到对象的全部符号属性。
  9. Object.defineProperty()Object.defineProperties() 对符号属性也有效。
  10. 知名符号7.6,以往的内部实现是不对开发者开放的,现在有了这些知名符号属性,可让开发者自信改变一些功能和接口的默认行为。

Sets 和 Maps

  • set 集合是一组没有重复元素的一个序列。
  • map key 值得集合,指向对应的值

ECMAScript 5 中的 Sets 和 Maps

在 es6 以前会有各类 sets/maps 的实现方式,可是大都或多或少有所缺陷。

背景

好比: 使用对象属性实现

let st = Object.create(null)

set.foo = true

if (set.foo) {
  // sth
}
复制代码

在将对象做为 set 或 map 使用的时候惟一的区别在于:

map 里面的 key 有存储对应的具体内容,而不像 set 仅仅用来存储 true or false,

用来标识 key 是否存在。

let map = Object.create(null)

map.foo = 'bar'

let value = map.foo

console.log(value) // 'bar'
复制代码

+RESULTS:

bar
复制代码

潜在问题

使用对象实现 set/map 的问题:

  1. 没法避免字符串 key 的惟一性问题
  2. 没法避免对象做为 key 的惟一性问题

字符串做为 key :

let map = Object.create(null)

map[5] = 'foo'

console.log(map["5"]) // 'foo'
复制代码

+RESULTS:

foo
复制代码

由于对于对象来讲,使用数字下表去访问的时候,其实是将下标数值转成字符串去访问了,

即至关于 map[5] 等价于 map['5'] 所以,有上面的结果输出。

可是,你恰恰想使用 5 和 '5' 去标识两个 key 的时候就没法达到目的了。

对象做为 key :

let map = Object.create(null),
    key1 = {},
    key2 = {}

map[key1] = 'foo'

console.log(map[key2]) // 'foo'
复制代码

+RESULTS:

foo
复制代码

对象做为 key 值得时候,内部会发生类型转换,将对象转成 "[object Object]"

所以不管用 key1 仍是 key2 去访问 map ,最后的结果都是 map["[object Object]"] 去访问了

所以,结果都是 'foo'。

Sets 集合

  1. 建立使用 new Set() 建立实例。
  2. 添加使用 set.add() 方法。
  3. 集合区分数值的数字类型和字符串类型,不会发生类型强转。
  4. -0+0 在集合中会被当作同样处理
  5. 对象能够做为 set 的元素,且两个 {} 会被当作两个不一样的元素处理

set 初始化

new Set() 建立了一个空的 set

能够在初始化的时候传入一个数组。

{% note info %}
实际上, Set 构造函数能够接受任意一个 iterable 对象做为参数。
{% endnote %}

let set = new Set([1, 2, 3, 4])

console.log(set.size) // 4
复制代码

+RESULTS:

4
复制代码

添加元素 set.add()

添加的元素区分类型,不会作类型转换,即 5'5' 是不同的,重复添加也只会执行一次,

set 的元素是不会重复的。

let set = new Set()

set.add(5)
set.add('5')
set.add(5)

console.log(set.size, set)
复制代码

+RESULTS:

2 Set { 5, '5' }
复制代码

对象元素:

let set = new Set(),
    key1 = {},
    key2 = {}

set.add(key1)
set.add(key2)
set.add(key1)

console.log(set.size) // 2
复制代码

+RESULTS:

2
复制代码

set apis

  1. set.has(v) 判断 set 中是否有元素 v ,返回 true/false
  2. set.add(v) 添加元素
  3. set.size 集合大小
  4. set.delete(v) 删除元素
  5. set.clear() 清空集合

集合迭代(forEach)

对集合使用 forEach 和对数组使用的方法同样,它接受一个函数,抓个函数又三个参数:

  1. 第一个参数:集合的当前值
  2. 第二个参数:和第一个参数同样是当前元素的值,跟数组不同,数组使用 forEach 抓个参数是当前索引值
  3. 第三个参数:被遍历的集合自己。

Sets 没有 Key 值。

let set = new Set(['a', 'b', 'c', 'd', 'e'])

console.log(set[0]) // undefined, 没有下标值
set.forEach(function(idx, v, ownerSet) {
  console.log(idx, v, ownerSet === set, ownerSet)
})
复制代码

+RESULTS:

undefined
a a true Set { 'a', 'b', 'c', 'd', 'e' }
b b true Set { 'a', 'b', 'c', 'd', 'e' }
c c true Set { 'a', 'b', 'c', 'd', 'e' }
d d true Set { 'a', 'b', 'c', 'd', 'e' }
e e true Set { 'a', 'b', 'c', 'd', 'e' }
复制代码

结果所示:

  1. 集合的 key 就是 value。
  2. 遍历的函数第三个参数 ownerSet 就是被遍历的 set 集合自己。

在使用 forEach 能够给它传递一个上下文参数,让绑定回调函数里面的 this

let set = new Set([1,2])

let processor = {
  output(value) {
    console.log('output from processor: ' + value)
  },

  process(dataSet, scope = 1) {
    const obj = {
      output(value) {
        console.log('output from obj: ' + value)
      }
    }
    dataSet.forEach(function(value) {
      this.output(value)
    }, scope === 1 ? this : obj)
  }
}

processor.process(set) // scope: processor
processor.process(set, 2) // scope: obj
复制代码

+RESULTS:

output from processor: 1
output from processor: 2
output from obj: 1
output from obj: 2
复制代码
  1. this 传递给回调,从而 output 来自 processor
  2. obj 传递给回调,从而 output 来自 obj

结论:*咱们能够经过给 forEach 传递第二个参数来改变回调函数的执行上下文。*

使用箭头函数解决 this 指向问题:

let set = new Set([1,2])

let processor = {
  output(value) {
    console.log('output from processor: ' + value)
  },

  process(dataSet) {
    // this 老是绑定到 processor
    dataSet.forEach(value => this.output(value), {})
  }
}

processor.process(set) // scope: processor

复制代码

+RESULTS:

output from processor: 1
output from processor: 2
复制代码

不管第二个参数 {} 传或不传结果都同样,箭头函数里的 this 指向不会发生改变。

{% note warning %}
集合不能直接使用索引访问元素,若是须要使用到索引访问元素,那最好将集合转成数组来使用。
{% endnote %}

Set 和 Array 之间的转换

  1. 集合转数组 let set = new Set([1, 2, 3, 2]); ,且会将重复的元素去掉只余一个。
  2. 数组转集合,最简单的就是展开符了 let arr = [...set];

展开符(…)能够做用域任何 iterable 的对象。即任何可 iterable 的对象均可以经过 ... 转成数组。

也由于有了 Set... 从而是数组的去重变得异常简单:

const eleminateDuplicates = items => [...new Set(items)]

let nums = [1, 2, 3, 2, 4, 3, 4]

console.log(eleminateDuplicates(nums).toString())
复制代码

+RESULTS:

1,2,3,4
复制代码

弱集(Weak Sets)

由于它存储对象引用的方式,集合类型也能够叫作强集合类型。

即集合中对于对象的存储是存储了该对象的引用而不是被添加到集合是的那个变量名而已,

相似对象的属性的值为对象同样,就算改变了这个属性的值,那个对象若是有其余变量指向它,

那他同样存在(相似 C 的指针概念,两个指针同时指向一块内存,一个指针的指向发生变化并不会

影响另外一个指针指向这块内存)。

好比:

let animal = {
  dog: {
    name: 'xxx',
    age: 10
  }
}

let dog1 = animal.dog

console.log(dog1.name) // 'xxx'
// 引用发生变化
animal.dog = null

// 并不影响别的变量指向 { name: 'xxx', age: 10 } 这个对象
console.log(dog1.age) // 10

// 指回去,依旧是它原来指向的那个对象
animal.dog = dog1
console.log(animal.dog.name) // 'xxx'
console.log(animal.dog.age) // 10

复制代码

+RESULTS:

xxx
10
xxx
10
复制代码

根据引用的特性,对于集合元素也同样实用:

let set = new Set();
let key = {};

set.add(key) // 实际将对象的引用加到集合中

console.log(set) // 1
console.log(set.size) // 1

key = null // 改变了变量值而已,实际引用的那个对象还在
console.log(set.size) // 1

key = [...set][0]

console.log(key)// {}
复制代码

+RESULTS:

Set { {} }
1
1
{}
undefined
复制代码

这种强引用在某些状况下极可能会出现内存泄漏,好比,在浏览器环境中

集合中保存了一些 DOM 元素的引用,而这些元素自己可能会被其余地方的

代码从 DOM 树中移除,同时你也不想再保有这些 DOM 元素的引用了,或者说之后

都不会用到它了,应该被释放回收才对,可是实际上集合中仍然保有这些元素的引用(实际已经不存在的东西),

这种状况就叫作内存泄漏(memory leak)。

为了解决这种状况, ECMAScript 6 中增长了一种集合类型: weak sets ,弱引用只会保存对象的弱引用

而不会保存引用的原始值。弱引用不会阻止垃圾回收若是它仅仅只是保存了引用而不是原始值。

建立 Weak Sets(WeakSet)

弱引用集合构造函数: WeakSet

let set = new WeakSet(),
    key = {}, key1 = key

set.add(key)

console.log(set)
key = null
console.log(set.has(key))
console.log(set.has(key1))
console.log(set.has(null))
console.log(set)
复制代码

+RESULTS:

WeakSet { [items unknown] }
false
true
false
WeakSet { [items unknown] }
undefined
复制代码

浏览器环境输出结果:

img

Set 和 WeakSet 对比

Set 中添加对象,添加的是对该对象的引用,所以保存该对象的变量值发生变化,并不影响该对象在集合中的事实。

WeakSet 中添加的是该变量的原始值??变量值一旦改变,集合中的内容将随之改变(由 JavaScript 引擎处理)。

{% note info %}
TODO: Set 保存引用?WeekSet 保存原始值??有啥区别??
{% endnote %}

这里咱们将对比两种集合在不一样形式下的运行结果,经过对比分析来搞清楚集合中引用和原始值的概念。

Set, WeakSet 添加对象的结果

let set = new Set()
let key = { a: 1 }

set.add(key)
console.log(set)
console.log(set.has(key)) // true

let wset = new WeakSet()
let wkey = { a: 1 }

wset.add(wkey)
console.log(wset)
console.log(wset.has(wkey))
复制代码

+RESULTS:

Set { { a: 1 } }
true
WeakSet { [items unknown] }
true
undefined
复制代码

这里 WeakSet 结果不直观,下面是浏览器结果:

img

从浏览器端的结果分析:

  1. 二者在内部属性 Entries 中都有一个咱们添加的 {a : 1} 对象元素。
  2. WeakSet 没有 size 属性, Set 有 size 属性。

改变对象 key/wkey 的值

let set = new Set()
let key = { a: 1 }

set.add(key)
console.log(set) // 改变以前
key = null
console.log(set) // 改变以后
console.log(set.has(key)) // true

let wset = new WeakSet()
let wkey = { a: 1 }

wset.add(wkey)
console.log(wset) // weak key 改变以前
wkey = null
console.log(wset) // weak key 改变以后
console.log(wset.has(wkey))

复制代码

+RESULTS: emacs nodejs

Set { { a: 1 } }
Set { { a: 1 } }
false
WeakSet { [items unknown] }
WeakSet { [items unknown] }
false
undefined
复制代码

浏览器环境输出结果:

img

结果:

  1. 对于 Set 对象变量 key 值得改变并不会影响 Set 中 {a:1} 对象

    Set 存放的是对象 {a:1} 的引用,即在 set.add(key) 以后,其实是有两个引用指向了
    {a:1} 对象,一个是 key 这个变量,一个是集合 set 中的某个位置上的变量(假设为: fkey)。
    根据引用的特性, key 的释放并不会影响 {a:1} 这个对象自己在内存中的存在,即不会影响 fkey
    对这个对象的影响,从而并不影响 set 的内容。

  2. WeakSet 中的 {a:1} 没有了

    WeakSet 咱们说它添加的是 wkey 的原始值,即便直接和 wkey 这个变量的原始值挂钩的,
    执行 wkey = null 就是讲它的原始值发生改变,最终将影响 WeakSet 。

针对 #2 中的 WeakSet 状况,将程序改造一下:

let set = new Set()
let key = { a: 1 }
let key1 = key

set.add(key)
console.log(set) // 改变以前
key = null
console.log(set) // 改变以后
console.log(set.has(key)) // true

console.log('-------- 楚河汉界 ---------')
let wset = new WeakSet()
let wkey = { a: 1 }
let wkey1 = wkey

wset.add(wkey)
console.log(wset) // weak key 改变以前
wkey = null
console.log(wset) // weak key 改变以后
console.log(wset.has(wkey))
console.log(wset.has(wkey1))
复制代码

+RESULTS:

Set { { a: 1 } }
Set { { a: 1 } }
false
-------- 楚河汉界 ---------
WeakSet { [items unknown] }
WeakSet { [items unknown] }
false
true
undefined
复制代码

再来看看输出结果:

img

咱们获得了使人意外的结果:

  1. 并无显示的 wset.add(wkey1) 可是最后的 wset.has(wkey1) 的结果倒是 true
  2. wset 集合中的 {a:1} 依然存在。

要理解这个问题,则须要知道“强引用”和“弱引用”的区别:

强引用和弱引用

咱们都知道 JavaScript 的垃圾回收机制中有一个相关知识点就叫作引用计数,即一个对象若是有被其余变量

引用那么这个对象的引用计数就 +1 若是这个变量被释放该对象的引用计数就 -1 一旦引用计数为 0

垃圾回收机制就会将这个对象回收掉,由于没有人再使用它了。

*强引用(Set)*:至关于让该对象的引用计数 +1 ,如 Set 集合保存了对象的引用导
致引用计数 +1 ,在拥有该对象的变量 key 的值怎么变化都不会致使引用计数为 0 从而阻止了垃圾回收器将其回收掉。

弱引用(WeakSet): 对对象的引用不会计入到引用计数中,即将 wkey 加入到 WeakSet 中,并不会引发

wkey 指向的那个对象的引用计数 +1 ,所以只要释放了 wkey 对其的引用,对象的引用计数就变成 0 了,所以

此时只有 wkey 指向 {a:1} 这个对象,改变 wkey 就会改变 WeakSet 中的内容,由于这个内容已经被

回收掉了。

根据上面的结论,咱们就知道为何咱们增长了一行 let key1 = key 以后, {a:1} 对象依然会在 wset 中由于此时 {a:1} 引用计数不为 0 并无被释放掉。

Maps

es6 的 Map 类型是一个有序的键值对列表, key 和 value 能够是任意类型,而且 key
不会发生类型强转,也就是说 5"5" 属于不一样的两个键,和对象不同(对象把他
们当作一个键,由于对象的 key 最终表示形式为 string 内部有发生强制转换)。

map.set(key, value)map.get(key)

Map 实例能够经过 setget 方法去设置键值对而后获取该值。

let map = new Map()
map.set('title', 'u es6')
map.set('year', 2019)

console.log(map)
console.log(map.get('title'))
console.log(map.get('year'))
console.log(map[0])
复制代码

+RESULTS:

Map { 'tit
u es6
2019
undefined
复制代码

map 数据的内部存储格式({ 'key' => value }):

img

新增函数列表

分类 函数名 描述 其余
Function
Object Object.is(v1, v2) v1 是不是 v2 弥补 === 不能判断 +0,-0 和 NaN,NaN
Object.assign(target, ...sources) 合并对象,浅拷贝,赋值运算
Object.getPrototypeOf(obj) 取原型对象
Object.setPrototypeOf(obj, protoObj) 设置原型对象
Object.getOwnPropertySymbols(obj) 获取对象全部符号属性 Object.keys, Object.getOwnPropertyNames 不能取符号属性
String str.codePointAt(n) Unicode编码值 str.charCodeAt(n)
str.fromCodePoint(s) 根据编码转字符 str.fromCharCode(s)
str.normalize() 将字符的不一样表示方式统一成一种表示形式 undefined, "NFC", "NFD", "NFKC", or "NFKD"
str.repeat(n) 将字符串重复 n 遍,做为新字符串返回 'x'.repeat(3) => 'xxx'
RexExp
相关文章
相关标签/搜索