mowatermelon / learn-es6

一个有趣的人写的有趣的前端基础
http://blog.iiwhy.cn/learn-es6
7 stars 5 forks source link

完整学习阮一峰老师的es6指南 #1

Open mowatermelon opened 5 years ago

mowatermelon commented 5 years ago

第一章 ECMAScript 6 简介

ES6的目标,是使得JavaScript语言可以用来编写大型的复杂应用程序,成为企业级开发语言。

ECMAScriptJavaScript语言的国际标准,JavaScriptECMAScript的实现。

第一小节 基础前置知识

1.1 ECMAScript的历史

1996年11月,JavaScript的创造者Netscape公司,决定将JavaScript提交给国际标准化组织ECMA,希望这种语言能够成为国际标准。次年,ECMA发布262号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言成为ECMAScript。这个版本就是ECMAScript 1.0版。

1998年6月,ECMAScript 2.0版发布。

1999年12月,ECMAScript 3.0 版发布,成为JavaScript大的通行标准,得到了广泛支持。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。

2007年10月,ECMAScript 4.0 版 草案发布,对3.0版做了大幅升级,原计划次年8月发布正式版本。然而在草案发布后,由于4.0版的目标过于激进,各方对于是否通过这个标准,产生了严重分歧。以YahooMicrosoftGoogle为首的大公司,反对JavaScript的大幅升级,主张小幅改动,而以JavaScript创造者Brendan Eich为首的Mozilla公司,则坚持当前的草案。

2008年7月,由于对于 下一个版本应该包含哪些功能,各方面分歧太大,争论过于激进,ECMA开会决定,中止ECMAScript 4.0的开发,将其中设计现有功能改善的一小部分,发布为ECMAScript 3.1,二将其他激进的设想扩大范围,放入以后的版本,鉴于会议的气氛,该版本的项目代号取名为Harmony(和谐),会后不久,ECMAScript 3.1就改名为ECMAScript 5

2009年12月,ECMAScript 5.0版正式发布。Harmony项目则一分为二,一些较为可行的设想定名为JavaScript.next继续开发,后来演变成ECMAScript 6,一些不是很成熟的设想,则被视为JavaScript.next.next,在更远的将来再考虑推出。

2011年6月,ECMAScript 5.1版本发布,并且成为ISO国际标准(ISO/IEC 16262:2011)。

2013年3月,ECMAScript 6 草案冻结,不再添加新功能,新功能设想将被放到 ECMAScript 7

2013年12月,ECMAScript 6 草案发布,此后是12个月的讨论期,以听取各方反馈意见。

2015年6月,ES6 的第一个版本发布,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。

2016年6月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布。


1.2 与 JavaScript 的关系

一个常见的问题是,ECMAScriptJavaScript 到底是什么关系?

要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。

该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性中立性

因此,ECMAScriptJavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScriptActionScript)。日常场合,这两个词是可以互换的。


1.3 与 ES2015 的关系

ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢?

2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。

但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。

但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。

如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。

标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。

接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。

ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,20176 月发布 ES2017 标准。

因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015ES2016ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指下一代 JavaScript 语言


1.4 语法提案的批准流程

任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。

一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站gitHub.com/tc39/ecma262查看。

本书的写作目标之一,是跟踪 ECMAScript 语言的最新进展,介绍 5.1 版本以后所有的新语法。对于那些明确或很有希望,将要列入标准的新语法,都将予以介绍。


1.5 ES6支持情况

各大浏览器的最新版本,对 ES6 的支持可以查看kangax.github.io/compat-table/es6/。随着时间的推移,支持度已经越来越高了,超过 90%ES6 语法特性都实现了。

image

NodeJavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。

// Linux & Mac
$ node --v8-options | grep harmony

// Windows
$ node --v8-options | findstr harmony

node --v8-options | findstr harmony
  --es-staging (enable test-worthy harmony features (for internal use only))
  --harmony (enable all completed harmony features)
  --harmony-shipping (enable all shipped harmony features)
  --harmony-do-expressions (enable "harmony do-expressions" (in progress))
  --harmony-class-fields (enable "harmony fields in class literals" (in progress))
  --harmony-static-fields (enable "harmony static fields in class literals" (in progress))
  --harmony-array-flatten (enable "harmony Array.prototype.flat{ten,Map}" (in progress))
  --harmony-locale (enable "Intl.Locale" (in progress))
  --harmony-public-fields (enable "harmony public fields in class literals")
  --harmony-private-fields (enable "harmony private fields in class literals")
  --harmony-numeric-separator (enable "harmony numeric separator between digits")
  --harmony-string-matchall (enable "harmony String.prototype.matchAll")
  --harmony-string-trimming (enable "harmony String.prototype.trim{Start,End}")
  --harmony-sharedarraybuffer (enable "harmony sharedarraybuffer")
  --harmony-regexp-named-captures (enable "harmony regexp named captures")
  --harmony-regexp-property (enable "harmony Unicode regexp property classes")
  --harmony-function-tostring (enable "harmony Function.prototype.toString")
  --harmony-promise-finally (enable "harmony Promise.prototype.finally")
  --harmony-optional-catch-binding (enable "allow omitting binding in catch blocks")
  --harmony-import-meta (enable "harmony import.meta property")
  --harmony-bigint (enable "harmony arbitrary precision integers")
  --harmony-dynamic-import (enable "harmony dynamic import")
  --harmony-array-prototype-values (enable "harmony Array.prototype.values")

我写了一个工具 ES-Checker,用来检查各种运行环境对 ES6 的支持情况。访问ruanyf.github.io/es-checker,可以看到您的浏览器支持 ES6 的程度。运行下面的命令,可以查看你正在使用的 Node 环境对 ES6 的支持程度。

$ npm install -g es-checker
$ es-checker

=========================================
Passes 24 feature Detections
Your runtime supports 57% of ECMAScript 6
=========================================

第二小节 通过Babel使用ES6

2.1 不同环境下使用babel

2.1.1 node环境的用法

安装环境依赖

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。

这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。

// 转码前
input.map(item => item + 1);

// 转码后
input.map(function (item) {
  return item + 1;
});

上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。

下面的命令在项目目录中,安装 Babel

$ npm install --save-dev @babel/core

配置文件.babelrc(低版本<7)

Babel 6 的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 6及以下版本 的第一步,就是配置这个文件。

该文件用来设置转码规则和插件,基本格式如下。

{
  "presets": [],
  "plugins": []
}

presets字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。

# 最新转码规则
$ npm install --save-dev @babel/preset-env

# react 转码规则
$ npm install --save-dev @babel/preset-react

然后,将这些规则加入.babelrc


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

注意,以下所有 Babel 6 工具和模块的使用,都必须先写好.babelrc

配置文件babel.config.js(高版本>=7)

Babel 7 的配置文件是babel.config.js,存放在项目的根目录下。使用 Babel 7及以上版本 的第一步,就是配置这个文件。

该文件用来设置转码规则和插件,基本格式如下。

module.exports = function (api) {
  api.cache(true);

  const presets = [ ... ];
  const plugins = [ ... ];

  return {
    presets,
    plugins
  };
}

注意,以下所有 Babel 7及以上版本 工具和模块的使用,都必须先写好babel.config.js

更多配置

在Webpack添加配置

module: {
  rules: [{
    loader: "babel-loader",
    options: {
      rootMode: "upward",
    }
  }]
}

在package.json添加配置

{
  "name": "my-package",
  "version": "1.0.0",
  "babel": {
    "presets": [ ... ],
    "plugins": [ ... ],
  }
}

2.1.2 命令行转换

Babel 提供命令行工具@babel/cli,用于命令行转码。

它的安装命令如下。

$ npm install --save-dev @babel/cli

基本用法如下

# 转码结果输出到标准输出
$ npx babel example.js

# 转码结果写入一个文件
# --out-file 或 -o 参数指定输出文件
$ npx babel example.js --out-file compiled.js
# 或者
$ npx babel example.js -o compiled.js

# 整个目录转码
# --out-dir 或 -d 参数指定输出目录
$ npx babel src --out-dir lib
# 或者
$ npx babel src -d lib

# -s 参数生成source map文件
$ npx babel src -d lib -s

2.1.3 浏览器环境

Babel 也可以用于浏览器环境,使用[@babel/standalone](https://babeljs.io/docs/en/next/babel-standalone.html)模块提供的浏览器版本,将其插入网页。

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>

注意,网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。

2.1.4 在线转换

Babel 提供一个REPL 在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。

image

<script>
"use strict";

(function () {
  var -console;

  var a = [1, 2, 3, 4, 5, 6, 7];

  (-console = console).log.apply(-console, a);
});
</script>

2.2 babel功能说明

2.2.1 babel API

如果某些代码需要调用 BabelAPI 进行转码,就要使用@babel/core模块。

image


var babel = require('@babel/core');

// 字符串转码
babel.transform('code();', options);
// => { code, map, ast }

// 文件转码(异步)
babel.transformFile('filename.js', options, , function(err, result) {
  result.code;
  result.map;
  result.ast;
});

// 文件转码(同步)
babel.transformFileSync('filename.js', options);
// => { code, map, ast }

// Babel AST转码
babel.transformFromAst(ast, code, options);
// => { code, map, ast }

配置对象options,可以参看官方文档http://babeljs.io/docs/usage/options/

下面是一个例子。


var es6Code = 'let x = n => n + 1';
var es5Code = require('@babel/core')
  .transform(es6Code, {
    presets: ['@babel/env']
  })
  .code;

console.log(es5Code);
// '"use strict";\n\nvar x = function x(n) {\n  return n + 1;\n};'

上面代码中,transform方法的第一个参数是一个字符串,表示需要被转换的 ES6 代码,第二个参数是转换的配置对象


2.2.2 @babel/polyfill

Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API

比如IteratorGeneratorSetMapProxyReflectSymbolPromise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

举例来说,ES6Array对象上新增了Array.from方法。Babel 就不会转码这个方法。

如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

安装命令如下。

$ npm install --save-dev @babel/polyfill

然后,在脚本头部,加入如下一行代码。

import '@babel/polyfill';
// 或者
require('@babel/polyfill');

Babel 默认不转码的 API 非常多,详细清单可以查看babel-plugin-transform-runtime模块的[definitions.js](https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-runtime/src/definitions.js)文件。


2.2.3 @babel/register

@babel/register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js.jsx.es.es6后缀名的文件,就会先用 Babel 进行转码。

$ npm install --save-dev @babel/register

使用时,必须首先加载@babel/register


// index.js
require('@babel/register');
require('./es6.js');

然后,就不需要手动对index.js转码了。

$ node index.js
2

需要注意的是,@babel/register只会对require命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码,所以只适合在开发环境使用。


2.2.4 babel-node

@babel/node模块的babel-node命令,提供一个支持 ES6REPL 环境。它支持 NodeREPL 环境的所有功能,而且可以直接运行 ES6 代码。

首先,安装这个模块。

$ npm install --save-dev @babel/node

然后,执行babel-node就进入 REPL 环境。

$ npx babel-node
> (x => x * 2)(1)
2

babel-node命令可以直接运行 ES6 脚本。将上面的代码放入脚本文件es6.js,然后直接运行。

# es6.js 的代码
# console.log((x => x * 2)(1));
$ npx babel-node es6.js
2

第三小节 通过Traceur使用ES6

Google公司的Traceur编译器 ,可以将ES6代码编译为ES5代码,https//github.com/google/traceur-compiler

3.1 Node 环境的用法

TraceurNode 用法如下(假定已安装traceur模块)。

var traceur = require('traceur');
var fs = require('fs');

// 将 ES6 脚本转为字符串
var contents = fs.readFileSync('es6-file.js').toString();

var result = traceur.compile(contents, {
  filename: 'es6-file.js',
  sourceMap: true,
  // 其他设置
  modules: 'commonjs'
});

if (result.error)
  throw result.error;

// result 对象的 js 属性就是转换后的 ES5 代码
fs.writeFileSync('out.js', result.js);
// sourceMap 属性对应 map 文件
fs.writeFileSync('out.js.map', result.sourceMap);

3.2 命令行转换

作为命令行工具使用时,Traceur 是一个 Node 的模块,首先需要用 npm 安装。

$ npm install -g traceur

安装成功后,就可以在命令行下使用 Traceur 了。

Traceur 直接运行 ES6 脚本文件,会在标准输出显示运行结果,以前面的calc.js为例。

$ traceur calc.js
Calc constructor
9

如果要将 ES6 脚本转为 ES5 保存,要采用下面的写法。

$ traceur --script calc.es6.js --out calc.es5.js

上面代码的--script选项表示指定输入文件,--out选项表示指定输出文件。

为了防止有些特性编译不成功,最好加上--experimental选项。

$ traceur --script calc.es6.js --out calc.es5.js --experimental

命令行下转换生成的文件,就可以直接放到浏览器中运行。


3.3 浏览器环境

Traceur 允许将 ES6 代码直接插入网页。首先,必须在网页头部加载 Traceur 库文件。

<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
<script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
<script type="module">
  import './Greeter.js';
</script>

上面代码中,一共有 4script标签。第一个是加载 Traceur库文件,第二个和第三个是将这个库文件用于浏览器环境,第四个则是加载用户脚本,这个脚本里面可以使用 ES6 代码。

注意,第四个script标签的type属性的值是module,而不是text/javascript。这是 Traceur 编译器识别 ES6 代码的标志,编译器会自动将所有type=module的代码编译为 ES5,然后再交给浏览器执行。

除了引用外部 ES6 脚本,也可以直接在网页中放置 ES6 代码。

<script type="module">
  class Calc {
    constructor() {
      console.log('Calc constructor');
    }
    add(a, b) {
      return a + b;
    }
  }

  var c = new Calc();
  console.log(c.add(4,5));
</script>

正常情况下,上面代码会在控制台打印出9

如果想对 Traceur 的行为有精确控制,可以采用下面参数配置的写法。

<script>
  // Create the System object
  window.System = new traceur.runtime.BrowserTraceurLoader();
  // Set some experimental options
  var metadata = {
    traceurOptions: {
      experimental: true,
      properTailCalls: true,
      symbols: true,
      arrayComprehension: true,
      asyncFunctions: true,
      asyncGenerators: exponentiation,
      forOn: true,
      generatorComprehension: true
    }
  };
  // Load your module
  System.import('./myModule.js', {metadata: metadata}).catch(function(ex) {
    console.error('Import failed', ex.stack || ex);
  });
</script>

上面代码中,首先生成 Traceur 的全局对象window.System,然后System.import方法可以用来加载 ES6。加载的时候,需要传入一个配置对象metadata,该对象的traceurOptions属性可以配置支持 ES6 功能。如果设为experimental: true,就表示除了 ES6 以外,还支持一些实验性的新功能。


3.4 在线转换

Traceur 也提供一个在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。

image

上面的例子转为 ES5 代码运行,就是下面这个样子。

<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
<script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
<script>
$traceurRuntime.ModuleStore.getAnonymousModule(function() {
  "use strict";
  (function() {
    var $--0;
    var a = [1, 2, 3, 45, 5, 6];
    ($--0 = console).log.apply($--0, $traceurRuntime.spread(a));
  });
  return {};
});
</script>
mowatermelon commented 5 years ago

第二章 let和const命令

第一小节 前置知识了解

1.1 块级作用域

1.1.1 为什么需要?

ES5 只有全局作用域函数作用域,没有块级作用域,这带来很多不合理的场景。

第一种场景

内层变量可能会覆盖外层变量。

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。

但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。


第二种场景

用来计数的循环变量泄露为全局变量。

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。


1.1.2 ES6 的块级作用域

let实际上为 JavaScript 新增了块级作用域


function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

上面的函数有两个代码块,都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。

如果两次都使用var定义变量n,最后输出的值才是 10

ES6 允许块级作用域的任意嵌套

上面代码使用了一个五层的块级作用域。


{{{{{let insane = 'Hello World'}}}}};

外层作用域无法读取内层作用域的变量。


{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};

内层作用域可以定义外层作用域的同名变量


{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。


// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

1.1.3 与函数声明相关

ES5中函数声明

函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。

ES5 规定,函数只能在顶层作用域函数作用域之中声明,不能在块级作用域声明。


// 情况一
if (true) {
  function f() {}
}

// 情况二
try {
  function f() {}
} catch(e) {
  // ...
}

上面两种函数声明,根据 ES5 的规定都是非法的。

但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。


function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

上面代码在 ES5 中运行,会得到I am inside!,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。


// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());

ES6中函数声明

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。


// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

符合 ES6 的浏览器中运行,理论上会得到I am outside!。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。

但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?

因为实际运行的是下面的代码。


// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。

为了减轻因此产生的不兼容问题,ES6附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式


ES6中在块级作用域内声明函数规则

注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域函数声明当作let处理。

根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。

考虑到环境导致的行为差异太大,应该避免在块级作用域声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句


块级作用域中函数声明需要使用大括号

ES6块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

// 不报错
'use strict';
if (true) {
  function f() {}
}

// 报错
'use strict';
if (true)
  function f() {}

1.2 作用域提升

我们知道ES6之前没有块级作用域,只有全局作用域函数作用域

JS在执行脚本之前会先解析代码,在解析的时候会创建一个全局执行上下文,并将其中的变量函数都先拿出来,并给它们提前在内存中开辟好空间,变量暂时赋值为undefined函数则会提前声明,整个存储在内存中,这一步做完了再正式执行程序。

函数在执行的时候同理,也会先解析代码,创建一个函数执行上下文,将其中的变量函数提前准备好。

console.log(a); // undefined
var a = 1;
test(); // test is running
function test(){
  console.log('test is running')
}
b=2;

所以,当执行console.log(a)的时候,JS解析器已经提前把a定义好并赋值为undefined。可以在函数定义前就调用。

1.2.1 变量提升

我们在使用变量函数的时候,理解什么时候被初始化值的是至关重要。

变量提升是指在声明一个变量之前就使用了变量,在全局作用域中,只有使用var关键字声明变量才会变量提升变量提升的时候浏览器只知道有这么一个变量

但你下面定义的值还没有赋值给这个变量,这时候·的值是undefined的,等到浏览器执行到下面的代码的时候才是一个赋值的过程。

所以变量提升的时候没有初始化值。用var声明变量的时候会给window增加一个相同变量名属性,所以你也可以通过属性名的方式获取这个变量的值,当没有使用任何关键字声明时,只是给一个变量赋值时,变量也相当于给window增加一个相同变量名属性

1.2.2 函数提升

定义一个函数可以使用函数声明函数表达式,这两种方式在提升的时候也是有区别的,函数声明会提升到作用域顶部,在提升的时候会分配一个内存空间变量指向这个函数的内存空间

所以在定义一个函数之前是可以执行这个函数的,函数声明的方式定义函数会提升。而函数表达式就跟变量提升,仅仅只是声明,并没有给其赋值


// 函数声明语句
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

1.3 暂时性死区

暂时性死区(temporal dead zone,简称 TDZ),ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取使用变量

ES6 规定暂时性死区letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。

这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

1.4 ES6 声明变量

ES5 只有两种声明变量的方法:var命令和function命令。

ES6 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。

所以,ES6 一共有 6 种声明变量的方法。


1.5 顶层对象

ES5顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。

浏览器里面,顶层对象是window,但 NodeWeb Worker 没有window

浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self

Node 里面,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块

函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined

不管是严格模式,还是普通模式new Function('return this')(),总是会返回全局对象

但是,如果浏览器用了 CSPContent Security Policy内容安全策略),那么evalnew Function这些方法都可能无法使用。

综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。

// 方法一
(typeof window !== 'undefined'
   ? window
   : (typeof process === 'object' &&
      typeof require === 'function' &&
      typeof global === 'object')
     ? global
     : this);

// 方法二
var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

现在有一个提案,在语言标准的层面,引入global作为顶层对象。也就是说,在所有环境下,global都是存在的,都可以从它拿到顶层对象

垫片库system.global模拟了这个提案,可以在所有环境拿到global

// CommonJS 的写法
require('system.global/shim')();

// ES6 模块的写法
import shim from 'system.global/shim'; shim();

上面代码可以保证各种环境里面,global对象都是存在的。


// CommonJS 的写法
var global = require('system.global')();

// ES6 模块的写法
import getGlobal from 'system.global';
const global = getGlobal();

上面代码将顶层对象放入变量global


1.6 顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1

a = 2;
window.a // 2

上面代码中,顶层对象属性赋值与全局变量赋值,是同一件事。

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。

这样的设计带来了几个很大的问题

ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令function命令声明的全局变量,依旧是顶层对象属性

另一方面规定,let命令const命令class命令声明的全局变量,不属于顶层对象属性

也就是说,从 ES6 开始,全局变量将逐步与顶层对象属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

上面代码中,全局变量avar命令声明,所以它是顶层对象属性

全局变量blet命令声明,所以它不是顶层对象属性,返回undefined


第二小节 let 命令

2.1 基本用法

2.1.1 基础说明

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

上面代码代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。


2.1.2 for循环使用

for循环的计数器,就很合适使用let命令,可以减少全局变量污染变量作用域错误访问

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

console.log(i);
// ReferenceError: i is not defined

上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。


var和let在for循环中使用对比

下面的代码如果使用var,最后输出的是10

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i

每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i

也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

上面代码中,变量ilet声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。


for循环单独作用域

for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域

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

上面代码正确运行,输出了 3abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域


2.2 注意事项

2.2.1 不存在变量提升

var命令会发生变量提升现象,即变量可以在声明之前使用,值为undefined

这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用

为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明使用,否则报错。


// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined

变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。


2.2.2 暂时性死区问题

基础案例

只要块级作用域内存在let命令,它所声明的变量就绑定binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

上面代码中,在let命令声明变量tmp之前,都属于变量tmp死区


ES6中typeof的不安全性

暂时性死区也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

上面代码中,变量x使用let命令声明,所以在声明之前,都属于x死区,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError

作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

typeof undeclared_variable // "undefined"

上面代码中,undeclared_variable是一个不存在的变量名,结果返回undefined

所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。

现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。


ES6中隐蔽死区

有些死区比较隐蔽,不太容易发现。

function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 报错
// Uncaught ReferenceError: y is not defined

上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于死区

如果y的默认值是x,就不会报错,因为此时x已经声明了。

function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2]

另外,下面的代码也会报错,与var的行为不同。

// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined

上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。

上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错x 未定义


2.2.3 不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}

因此,不能在函数内部重新声明参数。

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}

第三小节 const 命令

3.1 基本用法

const声明一个只读常量

一旦声明,常量的值就不能改变


const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

上面代码表明改变常量的值会报错。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。


3.2 实现原理

const实际上保证的,并不是变量不得改动,而是变量指向的那个内存地址所保存的数据不得改动

对于简单类型的数据(数值字符串布尔值),就保存在变量指向的那个内存地址,因此等同于常量

但对于复合类型的数据(主要是对象数组),变量指向的内存地址,保存的只是一个指向实际数据指针const只能保证这个指针固定的(即总是指向另一个固定地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

因此,将一个对象声明为常量必须非常小心。

声明对象常量


const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

上面代码中,常量foo储存的是一个地址,这个地址指向一个对象

不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性


声明数组常量


const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错

上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。


冻结对象案例

如果真的想将对象冻结,应该使用Object.freeze方法。


const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量foo指向一个冻结对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结对象属性也应该冻结

下面是一个将对象彻底冻结的函数。


var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

3.3 注意事项

3.3.1 不允许声明不赋值


const foo;
// SyntaxError: Missing initializer in const declaration

上面代码表示,对于const来说,只声明不赋值,就会报错。


3.3.2 不存在变量提升

const的作用域与let命令相同:只在声明所在的块级作用域内有效。


if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

3.3.3 暂时性死区问题

const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。


if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

上面代码在常量MAX声明之前就调用,结果报错。


3.3.4 不允许重复声明

const声明的常量,也与let一样不可重复声明


var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
mowatermelon commented 5 years ago

第三章 变量的解构赋值

以前,为变量赋值,只能直接指定值,ES6允许按照一定模式,从数组对象中提取值,对变量进行赋值,这被成为解构(Desructuring)。

本质上,这种写法属于模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值。


第一小节 复杂数据结构

1.1 数组的解构赋值

1.1.1 基础用法

完全解构

(function (log) {
    // 基础数组解构
    const [a, b, c] = [1, 2, 3];
    log(a); // 1
    log(b); // 2
    log(c); // 3

    // 嵌套数组解构
    const [foo, [[bar], baz]] = [1, [[2], 3]];
    log(foo); // 1
    log(bar); // 2
    log(baz); // 3

    const [ , , third] = ["foo", "bar", "baz"];
    log(third); // "baz"

    const [x, , y] = [1, 2, 3];
    log(x); // 1
    log(y); // 3

    const [head, ...tail] = [1, 2, 3, 4];
    log(head); // 1
    log(tail); // [2, 3, 4]

    const [e, f, ...g] = ['a'];
    log(e); // "a"
    log(f); // undefined
    log(g); // []
})(console.log)

不完全解构

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

(function (log) {
    const [x, y] = [1, 2, 3];
    log(x); // 1
    log(y); // 2

    const [a, [b], d] = [1, [2, 3], 4];
    log(a); // 1
    log(b); // 2
    log(d); // 4
})(console.log)

上面两个例子,都属于不完全解构,但是可以成功。


Set结构解构

对于 Set 结构,也可以使用数组的解构赋值。

(function (log) {
    const [x, y, z] = new Set(['a', 'b', 'c']);
    log(x); // "a"
})(console.log)

Generator 函数解构

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值


(function (log) {
    function* fibs() {
        const a = 0;
        const b = 1;
        while (true) {
            yield a;
            [a, b] = [b, a + b];
        }
    }

    const [first, second, third, fourth, fifth, sixth,seventh] = fibs();
    // yield 等待的是未做值替换之前的a值
    // 第一次 a 为 0 ,b 为 1
    // 第二次 a 为 1 ,b 为 1
    // 第三次 a 为 1 ,b 为 2
    // 第四次 a 为 2 ,b 为 3
    // 第五次 a 为 3 ,b 为 5
    // 第六次 a 为 5 ,b 为 8
    log(first); // 0
    log(second); // 1
    log(third); // 1
    log(fourth); // 2
    log(fifth); // 3
    log(sixth); // 5
    log(seventh); // 8
})(console.log)

上面代码中,fibs是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。


1.1.2 设置默认值

基础使用

解构赋值允许指定默认值。

(function (log) {
    const [foo = true] = [];
    log(foo); // true

    const [x, y = 'b'] = ['a'];
    log(x);// 'a'
    log(y);// 'b'

    const [a, b = 'b'] = ['a', undefined];
    log(a);// 'a'
    log(b);// 'b'
})(console.log)

使用表达式

如果默认值是一个表达式,那么这个表达式惰性求值的,即只有在用到的时候,才会求值

(function (log) {
    function f() {
        console.log('aaa');
    }

    const [x = f()] = [1];
    log(x);// 1
})(console.log)

上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。

(function (log) {
    const x;
    if ([1][0] === undefined) {
        x = f();
    } else {
        x = [1][0];
    }
    log(x);// 1
})(console.log)

引用其他变量值

默认值可以引用解构赋值其他变量,但该变量必须已经声明

(function (log) {
    const [x1 = 1, y1 = x1] = [];
    log(x1);// 1
    log(y1);// 1

    const [x2 = 1, y2 = x2] = [2];
    log(x2);// 2
    log(y2);// 3

    const [x3 = 1, y3 = x3] = [1, 2];
    log(x3);// 1
    log(y3);// 2

    const [x4 = y4, y4 = 1] = [];     // ReferenceError: y is not defined
})(console.log)

上面最后一个表达式之所以会报错,是因为x4y4做默认值时,y4还没有声明


1.1.3 注意事项

解构不成功

如果解构不成功,变量的值就等于undefined


const [foo] = [];
const [bar, foo] = [1];

以上两种情况都属于解构不成功,foo的值都会等于undefined


不可遍历结构

如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。

(function (log) {
    // 报错
    const [foo1] = 1;// TypeError: 1 is not iterable
    const [foo2] = false;// TypeError: false is not iterable
    const [foo3] = NaN;// TypeError: NaN is not iterable
    const [foo4] = undefined;// TypeError: undefined is not iterable
    const [foo5] = null;// TypeError: null is not iterable
    const [foo6] = {};// TypeError: {} is not iterable
})(console.log)

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。


1.2 对象的解构赋值

1.2.1 基础用法

基础使用

解构不仅可以用于数组,还可以用于对象

对象解构数组有一个重要的不同。数组的元素是按次序排列的,变量取值由它的位置决定;

对象属性没有次序变量必须与属性同名,才能取到正确的值。

(function (log) {
    const {
        bar,
        foo
    } = {
        foo: "aaa",
        bar: "bbb"
    };
    log(foo); // "aaa"
    log(bar); // "bbb"

    const {
        baz
    } = {
        foo: "aaa",
        bar: "bbb"
    };
    log(baz); // undefined
})(console.log)

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。

第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined

变量名与属性名不一致

如果变量名属性名不一致,必须写成下面这样。

(function (log) {
    const {
        foo: baz
    } = {
        foo: 'aaa',
        bar: 'bbb'
    };
    log(baz); // "aaa"

    const obj = {
        first: 'hello',
        last: 'world'
    };
    const {
        first: f,
        last: l
    } = obj;
    log(f); // 'hello'
    log(l); // 'world'
})(console.log)

这实际上说明,对象解构赋值是下面形式的简写(参见《对象的扩展》一章)。

(function (log) {
    const {
        foo: foo,
        bar: bar
    } = {
        foo: "aaa",
        bar: "bbb"
    };
    log(foo); // "aaa"
    log(bar); // "bbb"
})(console.log)

也就是说,对象的解构赋值内部机制,是先找到同名属性,然后再赋给对应的变量

真正被赋值的是后者,而不是前者

(function (log) {
    const {
        foo: baz
    } = {
        foo: "aaa",
        bar: "bbb"
    };
    log(baz); // "aaa"
    log(foo); // ReferenceError: foo is not defined
})(console.log)

上面代码中,foo是匹配的模式baz才是变量

真正被赋值的是变量baz,而不是模式foo


解构对象方法

对象解构赋值,可以很方便地将现有对象的方法,赋值到某个变量

(function (log) {
    const {
        log: log1,
        sin,
        cos
    } = Math;
    log(log1); // [Function: log]
    log(sin); // [Function: sin]
    log(cos); // [Function: cos]
})(console.log)

上面代码将Math对象的对数正弦余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。


属性名表达式

由于数组本质是特殊的对象,因此可以对数组进行对象属性解构

(function (log) {
    const arr = [1, 2, 3];
    const {
        0: first,
        [arr.length - 1]: last
    } = arr;
    log(first); // 1
    log(last); // 3
})(console.log)

上面代码对数组进行对象解构。数组arr0键对应的值是1[arr.length - 1]就是2键,对应的值是3

方括号这种写法,属于属性名表达式(参见《对象的扩展》一章)。


基础嵌套对象解构

数组一样,解构也可以用于嵌套结构对象

(function (log) {
    const obj = {
        p: [
            'Hello',
            {
                y: 'Melon'
            }
        ]
    };

    const {
        p: [x, {
            y
        }]
    } = obj;
    log(x); // "Hello"
    log(y); // "Melon"
})(console.log)

注意,这时p模式,不是变量,因此不会被赋值

如果p也要作为变量赋值,可以写成下面这样。

(function (log) {
    const obj = {
        p: [
            'Hello',
            {
                y: 'Melon'
            }
        ]
    };

    const {
        p,
        p: [x, {
            y
        }]
    } = obj;
    log(x); // "Hello"
    log(y); // "Melon"
    log(p); // [ 'Hello', { y: 'Melon' } ]
})(console.log)

多重嵌套对象解构

数组一样,解构也可以用于嵌套结构对象

(function (log) {
    const node = {
        loc: {
            start: {
                line: 1,
                column: 5
            }
        }
    };

    const {
        loc,
        loc: {
            start
        },
        loc: {
            start: {
                line
            }
        }
    } = node;
    log(line); // 1
    log(loc); // { start: { line: 1, column: 5 } }
    log(start); // { line: 1, column: 5 }
})(console.log)

上面代码有三次解构赋值,分别是对locstartline三个属性解构赋值

注意,最后一次对line属性的解构赋值之中,只有line变量locstart都是模式,不是变量


结合数组进行嵌套赋值

(function (log) {
    const obj = {};
    const arr = [];

    ({
        foo: obj.prop,
        bar: arr[0]
    } = {
        foo: 123,
        bar: true
    });

    log(obj); // { prop: 123 }
    log(arr); // [ true ]
})(console.log)

1.2.2 设置默认值

对象解构也可以指定默认值

(function (log) {
    const {
        x1 = 3
    } = {};
    log(x1); // 3

    const {
        x2,
        y2 = 5
    } = {
        x2: 1
    };
    log(x2); // 1
    log(y2); // 5

    const {
        x3: y3 = 3
    } = {};
    // log(x3); // ReferenceError: x3 is not defined
    log(y3); // 3

    const {
        x4: y4 = 3
    } = {
        x: 5
    };
    // log(x4); // ReferenceError: x4 is not defined
    log(y4); // 3

    const {
        message: msg = 'Something went wrong'
    } = {};
    log(msg); // "Something went wrong"
})(console.log)

默认值生效的条件是,对象的属性值严格等于undefined

(function (log) {
    const {
        x1 = 3
    } = {
        x1: undefined
    };
    log(x1); // 3

    const {
        x2 = 3
    } = {
        x2: null
    };
    log(x2); // null

})(console.log)

上面代码中,属性x等于null,因为nullundefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。


1.2.3 注意事项

解构不成功

如果解构不成功,变量的值就等于undefined

(function (log) {
    const {foo} = {bar: 'baz'};
    log(foo); // undefined
})(console.log)

父属性不存在

如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错

(function (log) {
    // 报错
    const {foo: {bar}} = {baz: 'baz'};// TypeError: Cannot destructure property `bar` of 'undefined' or 'null'.

    const _tmp = {baz: 'baz'};
    _tmp.foo.bar // Cannot read property 'bar' of undefined
})(console.log)

上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象bar属性,解构时会报错。

原因很简单,因为foo这时等于undefined,类似于上文的_tmp.foo.bar,再取子属性就会报错。


已经声明的变量

如果要将一个已经声明变量用于解构赋值,必须非常小心。

(function (log) {
    // 错误的写法
    const x;
    {x} = {x: 1};
    // SyntaxError: Unexpected token =
})(console.log)

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误

只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

(function (log) {
    // 正确的写法
    const x;
    ({x} = {x: 1});
    log(x);// 1
})(console.log)

上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。


1.3 函数的解构赋值

1.3.1 基础用法

基础使用

函数参数也可以使用解构赋值

(function (log) {
    function add([x, y]) {
        return x + y;
    }

    log(add([1, 2])); // 3
})(console.log)

上面代码中,函数add参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量xy。对于函数内部的代码来说,它们能感受到的参数就是xy


结合箭头函数使用

函数参数也可以使用解构赋值

(function (log) {
    const arr = [
        [1, 2],
        [3, 4]
    ].map(([a, b]) => a + b);
    log(arr); // [ 3, 7 ]
})(console.log)

1.3.2 设置默认值

函数参数解构也可以使用默认值

函数对象参数属性使用默认值

(function (log) {
    function move({
        x = 0,
        y = 0
    } = {}) {
        return [x, y];
    }

    log(move({
        x: 3,
        y: 8
    })); // [3, 8]
    log(move({
        x: 3
    })); // [3, 0]
    log(move({})); // [0, 0]
    log(move()); // [0, 0]
})(console.log)

上面代码中,函数move参数是一个对象,通过对这个对象进行解构,得到变量xy的值。如果解构失败,xy等于默认值


函数对象参数使用默认值

(function (log) {
    function move({
        x,
        y
    } = {
        x: 0,
        y: 0
    }) {
        return [x, y];
    }

    log(move({
        x: 3,
        y: 8
    })); // [3, 8]
    log(move({
        x: 3
    })); // [3, undefined]
    log(move({})); // [undefined, undefined]
    log(move()); // [0, 0]
})(console.log)

上面代码是为函数move参数指定默认值,而不是为变量xy指定默认值,所以会得到与前一种写法不同的结果。


1.3.3 注意事项

undefined就会触发函数参数默认值

(function (log) {
    const arr = [1, undefined, 3].map((x = 'yes') => x);
    log(arr); // [ 1, 'yes', 3 ]
})(console.log)

第二小节 基础数据结构

2.1 字符串的解构赋值

2.1.1 转为数组解构

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

(function (log) {
    const [a, b, c, d, e] = 'hello';
    log(a); // "h"
    log(b); // "e"
    log(c); // "l"
    log(d); // "l"
    log(e); // "o"

})(console.log)

2.1.2 解构字符串长度

类似数组对象都有一个length属性,因此还可以对这个属性解构赋值。

(function (log) {
    const {
        length: len
    } = 'hello';
    log(len); // 5

})(console.log)

2.2 数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值布尔值,则会先转为对象

(function (log) {
    const {
        toString: s1
    } = 123;
    log(s1 === Number.prototype.toString); // true

    const {
        toString: s2
    } = true;
    log(s2 === Boolean.prototype.toString); // true
})(console.log)

上面代码中,数值布尔值包装对象都有toString属性,因此变量s1和变量s2都能取到


第三小节 圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。

对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构歧义,就不得使用圆括号

但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号

3.1 避免使用情况

不能使用圆括号的情况

以下三种解构赋值不得使用圆括号。

3.1.1 变量声明语句

(function (log) {
    // 全部报错
    const [(a)] = [1];// SyntaxError: Unexpected token (

    const {x: (c)} = {};// SyntaxError: Unexpected token (
    const ({x: c}) = {};// SyntaxError: Unexpected token (
    const {(x: c)} = {};// SyntaxError: Unexpected token (
    const {(x): c} = {};// SyntaxError: Unexpected token (

    const { o: ({ p: p }) } = { o: { p: 2 } };// SyntaxError: Unexpected token (
})(console.log)

上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号


3.1.2 函数参数

函数参数也属于变量声明,因此不能带有圆括号

// 报错
function f([(z)]) { return z; }// SyntaxError: Unexpected token (
// 报错
function f([z,(x)]) { return x; }// SyntaxError: Unexpected token (

3.1.3 赋值语句的模式

(function (log) {
    // 全部报错
    ({ p: a }) = { p: 42 };// SyntaxError: Unexpected token (
    ([a]) = [5];// SyntaxError: Unexpected token (
})(console.log)

上面代码将整个模式放在圆括号之中,导致报错。

(function (log) {
    // 报错
    [({ p: a }), { x: c }] = [{}, {}];// SyntaxError: Unexpected token (
})(console.log)

上面代码将一部分模式放在圆括号之中,导致报错。


3.2 可以使用情况

可以使用圆括号的情况只有一种:赋值语句非模式部分,可以使用圆括号

(function (log) {
    [(b)] = [3]; // 正确
    log(b);// 3
    ({ p: (d) } = {}); // 正确
    log(d);// undefined
    [(parseInt.prop)] = [3]; // 正确
    log(parseInt.prop);// 3
})(console.log)

上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;

其次它们的圆括号都不属于模式的一部分。

第一行语句中,模式是取数组的第一个成员,跟圆括号无关;

第二行语句中,模式是p,而不是d

第三行语句与第一行语句性质一致。

解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪赋值表达式

({} = [true, false]);
({} = 'abc');
({} = []);

上面的表达式虽然毫无意义,但是语法是合法的,可以执行。


第四小节 用途

4.1 交换变量的值

(function (log) {
    let x = 1;
    let y = 2;
    log(x);// 1
    log(y);// 2
    [x, y] = [y, x];
    log(x);// 2
    log(y);// 1

})(console.log)

上面代码交换变量xy的值,这样的写法不仅简洁,而且易读语义非常清晰。


4.2 从函数返回多个值

函数只能返回一个,如果要返回多个,只能将它们放在数组或者对象里返回。

有了解构赋值,取出这些就非常方便。

(function (log) {
    // 返回一个数组
    function example1() {
        return [1, 2, 3];
    }

    let [a, b, c] = example1();
    log(a); // 1
    log(b); // 2
    log(c); // 3

    // 返回一个对象
    function example2() {
        return {
            foo: 1,
            bar: 2
        };
    }
    let {
        foo,
        bar
    } = example2();

    log(foo); // 1
    log(bar); // 2
})(console.log)

4.3 函数参数的定义

解构赋值可以方便地将一组参数变量名对应起来。


// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

4.4 函数参数的默认值

指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

4.5 提取 JSON 数据

解构赋值对提取 JSON 对象中的数据,尤其有用。


(function (log) {
    const jsonData = {
        id: 42,
        status: "OK",
        data: [867, 5309]
    };

    const {
        id,
        status,
        data: number
    } = jsonData;

    log(id, status, number);
    // 42, "OK", [867, 5309]
})(console.log)

上面代码可以快速提取 JSON 数据的值。


4.6 遍历 Map 结构

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量解构赋值,获取键名键值就非常方便。

(function (log) {
    const map = new Map();
    map.set('first', 'hello');
    map.set('second', 'melon');

    for (let [key, value] of map) {
        log(key + " is " + value);
    }
    // first is hello
    // second is melon
})(console.log)

如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

4.7 获取指定模块的指定方法

加载模块时,往往需要指定需要获取哪些方法解构赋值使得获取语句非常清晰。


const {a,b} = require('xxx');

const { SourceMapConsumer, SourceNode } = require("source-map");

mowatermelon commented 5 years ago

第四章 字符串的扩展

第一小节 字符的 Unicode 表示法

1.1 基础说明

JavaScript 允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点

但是,这种表示法只限于\u0000-\uFFFF之间的字符,超过这个范围的字符,必须用两个双字节的形式表达。

如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),Javascript会理解为\u20BB+7,由于\u20BB是一个不可打印字符,所以会打印一个空格,后面跟着一个7

ES6对于这一点做了改进,只要将超过0xFFFF的编号放入大括号,就能得到正确解读。

(function (log) {
    log('\u0061');// 'a'

    log('\uD842\uDFB7');// 𠮷

    log('\u20BB7'); // ₻7

    log('\u{20BB7}'); // 𠮷

    log('\u{41}\u{42}\u{43}'); // ABC

    let hello = 123;
    log(hell\u{6F}); // 123

    log('\u{1F680}' === '\uD83D\uDE80');
})(console.log)

上面代码中,最后一个例子表明,大括号表示法四字节UTF-16 编码是等价的。


1.2 字符描述举例

有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符

(function (log) {
    log('\z' === 'z');  // true
    log('\172' === 'z'); // true
    log('\x7A' === 'z'); // true
    log('\u007A' === 'z'); // true
    log('\u{7A}' === 'z'); // true
})(console.log)

第二小节 原型对象方法扩展

2.1 fromCodePoint()

ES5 提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于0xFFFF)。

(function (log, S) {
    log(S.fromCharCode(0x20BB7)); // ஷ
})(console.log, String)

上面代码中,String.fromCharCode不能识别大于0xFFFF的码点,所以0x20BB7就发生了溢出,最高位2被舍弃了,最后返回码点U+0BB7对应的字符,而不是码点U+20BB7对应的字符

ES6 提供了String.fromCodePoint方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

(function (log, S) {
    log(S.fromCodePoint(0x20BB7)); // 𠮷
    log(S.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'); // true
})(console.log, String)

上面代码中,如果String.fromCodePoint方法有多个参数,则它们会被合并成一个字符串返回。

注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。


2.2 raw()

ES6 还为原生的 String 对象,提供了一个raw方法。

String.raw方法,往往用来充当模板字符串处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串

(function (log, S) {
    log(S.raw`Hi\n${2+3}!`);//  'Hi\\n5!'

    log(S.raw`Hi\u000A!`);//  'Hi\\u000A!'
})(console.log, String)

如果原字符串斜杠已经转义,那么String.raw会进行再次转义。

(function (log, S) {
    log(S.raw`Hi\\n`);//  'Hi\\n'
})(console.log, String)

String.raw方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。

String.raw方法也可以作为正常函数使用。

这时,它的第一个参数,应该是一个具有raw属性对象,且raw属性的值应该是一个数组

(function (log, S) {
    log(S.raw({
        raw: 'test'
    }, 0, 1, 2));
    // 't0e1s2t'

    // 等同于
    log(S.raw({
        raw: ['t', 'e', 's', 't']
    }, 0, 1, 2));// 't0e1s2t'
})(console.log, String)

作为函数,String.raw的代码实现基本如下。

String.raw = function (strings, ...values) {
  let output = '';
  let index;
  for (index = 0; index < values.length; index++) {
    output += strings.raw[index] + values[index];
  }

  output += strings.raw[index]
  return output;
}

第三小节 实例对象方法扩展

3.1 codePointAt()

3.1.1 产生原因

Javascript内部,字符以UTF-16的格式存储,每个字符固定为2个字节。

对于那些需要4个字节存储的字符(Unicode 编号大于0xFFFF的字符),Javascript会认为它们是个字符。

(function (log) {
    const s = '𠮷a';
    log(s.length) // 3
    log(s.charAt(0)) // �
    log(s.charAt(1)) // �

    log(s.charCodeAt(0)) // 55362
    log(s.charCodeAt(1)) // 57271
})(console.log)

上面代码中,汉字𠮷(注意,这个字不是吉祥)的码点0x20BB7UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。

对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt方法无法读取整个字符charCodeAt方法只能分别返回前两个字节和后两个字节


3.1.2 优化效果

ES6提供了codePointAt 方法,能够正确处理4个字节存储的字符,返回一个字符的Unicode码点。

(function (log) {
    const s = '𠮷a';

    log(s.codePointAt(0)) // 134071
    log(s.codePointAt(1)) // 57271
    log(s.codePointAt(2)) // 97
})(console.log)

codePointAt方法的参数,是字符字符串中的位置(从0开始)。

JavaScript𠮷a视为三个字符,codePointAt 方法在第一个字符上,正确地识别了𠮷,返回了它的十进制码点 134071(即十六进制的20BB7)。

在第二个字符(即𠮷的后两个字节)和第三个字符a上,codePointAt方法的结果与charCodeAt方法相同。

总之,codePointAt方法可以正确返回四字节UTF-16字符的Unicode码点。

对于那些两个字节存储的常规字符,它返回结果与charCodeAt方法相同。


3.1.3 结合for-of

你可能注意到了,codePointAt方法的参数,仍然是不正确的。

比如,下面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2

解决这个问题的一个办法是使用for...of循环,因为它会正确识别 32 位的 UTF-16 字符。

(function (log) {
    const s = '𠮷a';

    log(s.codePointAt(0)) // 134071
    log(s.codePointAt(2)) // 97

    for (let ch of s) {
        log(ch.codePointAt(0));
    }
    // 134071
    // 97
})(console.log)

3.1.4 进制转换

codePointAt方法返回的是码点十进制值,如果想要其他进制的值,可以使用toString方法转换一下。

(function (log) {
    const s = '𠮷a';
    log(s.codePointAt(0)) // 134071
    log(s.codePointAt(2)) // 97

    log(s.codePointAt(0).toString(2)) // 100000101110110111
    log(s.codePointAt(2).toString(2)) // 1100001

    log(s.codePointAt(0).toString(8)) // 405667
    log(s.codePointAt(2).toString(8)) // 141

    log(s.codePointAt(0).toString(10)) // 134071
    log(s.codePointAt(2).toString(10)) // 97

    log(s.codePointAt(0).toString(16)) // 20bb7
    log(s.codePointAt(2).toString(16)) // 61

    log(s.codePointAt(0).toString(32)) // 42tn
    log(s.codePointAt(2).toString(32)) // 31
})(console.log)

3.1.5 判断是否是多字节

codePointAt 方法是测试一个字符由两个字节还是四个字节组成的最简单方法。

(function (log) {
    function is32Bit(c) {
        return c.codePointAt(0) > 0xFFFF;
    }

    log(is32Bit('𠮷')); // true
    log(is32Bit('a')); // false
})(console.log)

3.2 normalize()

3.2.1 产生原因

许多欧洲语言有语调符号重音符号。为了表示它们,Unicode 提供了两种方法。

这两种表示方法,在视觉语义上都等价,但是 JavaScript 不能识别。

(function (log, S) {
    log('\u01D1' === '\u004F\u030C'); //false

    log('\u01D1'.length); // 1
    log('\u004F\u030C'.length); // 2
})(console.log, String)

上面代码表示,JavaScript合成字符视为两个字符,导致两种表示方法不相等。


3.2.2 优化效果

ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

(function (log, S) {
    const type1 = '\u01D1'.normalize();
    const type2 = '\u004F\u030C'.normalize();
    log(type1 === type2); // true
})(console.log, String)

3.2.3 参数详解

normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。

形参值 形参值解释
NFC 默认参数,表示标准等价合成Normalization Form Canonical Composition),返回多个简单字符合成字符。所谓标准等价指的是视觉语义上的等价。
NFD 表示标准等价分解Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符
NFKC 表示兼容等价合成Normalization Form Compatibility Composition),返回合成字符。所谓兼容等价指的是语义上存在等价,但视觉上不等价,比如喜喜。(这只是用来举例,normalize方法不能识别中文。)
NFKD 表示兼容等价分解Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符
(function (log, S) {
    log('\u004F\u030C'.normalize('NFC').length); // 1
    log('\u004F\u030C'.normalize('NFD').length); // 2
})(console.log, String)

上面代码表示,NFC参数返回字符的合成形式NFD参数返回字符的分解形式。

不过,normalize方法目前不能识别三个三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。


3.3 判断字符串包含

传统上,Javascript中只有indexOf方法,可用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法

方法名 方法解释
contains(findStr,findIndex) 返回布尔值,表示了是否找到参数字符串
startsWith(findStr,findIndex) 返回布尔值,表示参数字符串是否正在源字符串的头部
endsWith(findStr,findIndex) 返回布尔值,表示参数字符串是否正在源字符串的尾部

(function (log, S) {
    const s1 = 'Hello world!';

    log(s1.startsWith('Hello')); // true
    log(s1.endsWith('!')); // true
    log(s1.includes('o')); // true

    // 这三个方法都支持第二个参数,表示开始搜索的位置。

    const s2 = 'Hello world!';

    log(s2.startsWith('world', 6)); // true
    log(s2.endsWith('Hello', 5)); // true
    log(s2.includes('Hello', 6)); // false
})(console.log, String)

3.4 repeat

repeat(repeatIndex)返回一个新字符串,表示将原字符串重复repeatIndex

3.4.1 基础使用


(function (log, S) {
    log('x'.repeat(3)); // 'xxx'
    log('hello'.repeat(2)); // 'hellohello'
    log('na'.repeat(0)); // ''
})(console.log, String)

3.4.2 特殊形参值处理

参数值为小数

如果repeat的参数是小数,会被取整,注意这个取整对当前值不做任何四舍五入


(function (log, S) {
    log('na'.repeat(2.3)); // 'nana'
    log('na'.repeat(2.5)); // 'nana'
    log('na'.repeat(2.9)); // 'nana'

    log('na'.repeat(2.3999)); // 'nana'
    log('na'.repeat(2.599)); // 'nana'
    log('na'.repeat(2.999)); // 'nana'
})(console.log, String)

参数值为0

如果repeat的参数是+0,-0,0,NaN,-1~0之间或者0~1之间值,会返回一个空字符串

0-1 之间的小数,则等同于 0,这是因为会先进行取整运算。

0-1 之间的小数取整以后等于-0repeat视同为 0

(function (log, S) {
    log('na'.repeat(+0)); // ''
    log('na'.repeat(-0)); // ''
    log('na'.repeat(-0)); // ''

    log('na'.repeat(NaN)); // ''

    log('na'.repeat(+0.2)); // ''
    log('na'.repeat(+0.5)); // ''
    log('na'.repeat(+0.8)); // ''

    log('na'.repeat(-0.2)); // ''
    log('na'.repeat(-0.5)); // ''
    log('na'.repeat(-0.8)); // ''
})(console.log, String)

参数值为负数或者Infinity

如果repeat的参数是负数或者Infinity,会报错。

(function (log, S) {
    log('na'.repeat(Infinity)); // RangeError: Invalid count value
    log('na'.repeat(-1)); // RangeError: Invalid count value
})(console.log, String)

参数值为字符串

如果repeat的参数是字符串,则会先转换成数字

(function (log, S) {
    log(1, 'na'.repeat('na')); // 1 ''
    log(2, 'na'.repeat('3na')); // 2 ''
    log(3, 'na'.repeat('3,')); // 3 ''
    log(3, 'na'.repeat('3\s')); // 3 ''
    log(3, 'na'.repeat('3\S')); // 3 ''

    log(4, 'na'.repeat('3 ')); // 4 'nanana'
    log(4, 'na'.repeat('3\t')); // 4 'nanana'
    log(4, 'na'.repeat('\n3\n')); // 4 'nanana'

    log(5, 'na'.repeat('03')); // 5 'nanana'
    log(5, 'na'.repeat('0b1')); // 5 'na'
    log(5, 'na'.repeat('0o3')); // 5 'nanana'
    log(5, 'na'.repeat('0x3')); // 5 'nanana'

    // log('na'.repeat('-3')); // RangeError: Invalid count value

    log(6, 'na'.repeat('+3')); // 6 'nanana'
})(console.log, String)

参数值为日期格式

如果repeat的参数是日期格式,则会先转换成数字

但是由于转换的一般超过了可用的字符串长度,所以一般会直接报错RangeError: Invalid string length

如果是传入日期格式单独转换天数日期值repeat可以正常使用。

(function (log, S) {
    const d = Reflect.construct(Date,[]);
    log(d.getTime()); // 1549895917860
    log(d.getDate()); // 11
    log(d.getDay()); // 1

    // log('na'.repeat(d)); // RangeError: Invalid string length
    // log('na'.repeat(d.getTime())); // RangeError: Invalid string length
    log('na'.repeat(d.getDate())); // nanananananananananana
    log('na'.repeat(d.getDay())); // na
})(console.log, String)

参数值为数组或者对象

如果repeat的参数是数组MapWeakMapSetWeakSet或者对象,则会统一当作0进行处理,返回空字符串

(function (log, S) {
    log(1, 'na'.repeat([])); // 1 ''
    log(1, 'na'.repeat([1, 2, 3])); // 1 ''
    log(1, 'na'.repeat([{
        z: 1
    }])); // 1 ''

    log(2, 'na'.repeat({})); // 2 ''
    log(2, 'na'.repeat({
        z: 1
    })); // 2 ''

    log(3, 'na'.repeat(new Map())); // 3 ''

    log(4, 'na'.repeat(new WeakMap())); // 4 ''

    log(5, 'na'.repeat(new Set())); // 5 ''

    log(6, 'na'.repeat(new WeakSet())); // 6 ''
})(console.log, String)

3.5 字符串补全

3.5.1 基础使用

ES2017 引入了字符串补全长度的功能。

如果某个字符串不够指定长度,会在头部尾部补全。

方法名 方法解释
padStart(maxFillLength,fillStr) 返回最后补全的字符串,用于头部补全
padEnd(maxFillLength,fillStr) 返回最后补全的字符串,尾部补全
(function (log, S) {
    const s = 'm';
    const fillStr = 'ab';
    log(s.padStart(5, fillStr)); // ababm
    log(s.padStart(4, fillStr)); // abam

    log(s.padEnd(5, fillStr)); // mabab
    log(s.padEnd(4, fillStr)); // maba
})(console.log, String)

3.5.2 最大长度值忽略情况

如果原字符串长度,等于或大于最大长度,则字符串补全不生效,返回原字符串

(function (log, S) {
    const s = 'mm';
    const fillStr = 'ab';
    log(s.padStart(2, fillStr)); // 'mm'
    log(s.padEnd(2, fillStr)); // 'mm'
})(console.log, String)

3.5.3 补全字符串截取

如果用来补全的字符串原字符串,两者的长度之和超过了最大长度,则会截去超出位数补全字符串

(function (log, S) {
    const s = 'mm';
    const fillStr = '0123456789';
    log(s.padStart(10, fillStr)); // 01234567mm
    log(s.padEnd(10, fillStr)); // mm01234567
})(console.log, String)

3.5.4 补全字符串忽略

如果省略第二个参数,默认使用空格补全长度。

(function (log, S) {
    const s = 'mm';
    log(s.padStart(10)); //         mm
    log(s.padEnd(10)); // mm        
})(console.log, String)

3.5.5 常见用途

数值补全指定位数

padStart()padEnd常见用途是为数值补全指定位数

下面代码生成 10 位的数值字符串

(function (log, S) {
    const fillLength = 10;
    const fillStr = '0';
    log('1'.padStart(fillLength, fillStr)); // '0000000001'
    log('12'.padStart(fillLength, fillStr)); // '0000000012'
    log('123456'.padStart(fillLength, fillStr)); // '0000123456'

    log('1'.padEnd(fillLength, fillStr)); // '1000000000'
    log('12'.padEnd(fillLength, fillStr)); // '1200000000'
    log('123456'.padEnd(fillLength, fillStr)); // '1234560000'
})(console.log, String)

提示字符串格式

padStart()padEnd的还可以用作日期格式的字符串格式提示

下面代码生成年月日日期格式的字符串。

(function (log, S) {

    log('12'.padStart(10, 'YYYY-MM-DD')); // 'YYYY-MM-12'
    log('09-12'.padStart(10, 'YYYY-MM-DD')); // 'YYYY-09-12'

    // log('12'.padEnd(10, 'YYYY-MM-DD')); // 12YYYY-MM-
    // log('09-12'.padEnd(10, 'YYYY-MM-DD')); // 09-12YYYY-
    log('2019'.padEnd(10, '-MM-DD')); // '2019-MM-DD'
})(console.log, String)

3.6 matchAll

3.6.1 产生原因

如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出。

(function (log, S) {

    const regex = /t(e)(st(\d?))/g;
    const string = 'test1test2test3';

    const matches = [];
    let match;
    while (match = regex.exec(string)) {
      matches.push(match);
    }

    log(matches);
    //     [ [ 'test1',
    //     'e',
    //     'st1',
    //     '1',
    //     index: 0,
    //     input: 'test1test2test3',
    //     groups: undefined ],
    //   [ 'test2',
    //     'e',
    //     'st2',
    //     '2',
    //     index: 5,
    //     input: 'test1test2test3',
    //     groups: undefined ],
    //   [ 'test3',
    //     'e',
    //     'st3',
    //     '3',
    //     index: 10,
    //     input: 'test1test2test3',
    //     groups: undefined ] ]
})(console.log, String)

上面代码中,while循环取出每一轮的正则匹配,一共三轮。


3.6.2 优化结果

目前有一个提案,目前还未实现,增加了String.prototype.matchAll方法,可以一次性取出所有匹配

不过,它返回的是一个遍历器(Iterator),而不是数组

(function (log, S) {
    // g 修饰符加不加都可以
    const regex = /t(e)(st(\d?))/g;
    const string = 'test1test2test3';

    if (string.matchAll) {
        for (const match of string.matchAll(regex)) {
            log(match);
        }
        // ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
        // ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
        // ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
    } else {
        log('not support');
    }

    // not support
})(console.log, String)

上面代码中,由于string.matchAll(regex)返回的是遍历器,所以可以用for...of循环取出。

相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源


3.6.3 遍历器转换

遍历器转为数组是非常简单的,使用...运算符和Array.from方法就可以了。

(function (log, S) {
    // g 修饰符加不加都可以
    const regex = /t(e)(st(\d?))/g;
    const string = 'test1test2test3';

    if (string.matchAll) {
        const matches = string.matchAll(regex);
        // 转为数组方法一
        log([...matches]);
        //     [ [ 'test1',
        //     'e',
        //     'st1',
        //     '1',
        //     index: 0,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test2',
        //     'e',
        //     'st2',
        //     '2',
        //     index: 5,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test3',
        //     'e',
        //     'st3',
        //     '3',
        //     index: 10,
        //     input: 'test1test2test3',
        //     groups: undefined ] ]

        // 转为数组方法二
        log(Array.from(matches));
        //     [ [ 'test1',
        //     'e',
        //     'st1',
        //     '1',
        //     index: 0,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test2',
        //     'e',
        //     'st2',
        //     '2',
        //     index: 5,
        //     input: 'test1test2test3',
        //     groups: undefined ],
        //   [ 'test3',
        //     'e',
        //     'st3',
        //     '3',
        //     index: 10,
        //     input: 'test1test2test3',
        //     groups: undefined ] ]

    } else {
        log('not support');
    }

    // not support
})(console.log, String)

第四小节 模板字符串

4.1 产生原因

传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

4.2 优化结果

4.2.1 基础使用

之前写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

模板字符串(template string)增加版字符串,用反引号作为标识,它可以当作普通字符串使用,也可以用来定义多行字符串

使用模板字符串表示多行字符串,所有的空格缩进都会被保留在输出之中。

(function (log, S) {
    // 普通字符串
    log(`In JavaScript '\n' is a line-feed.`);
    // In JavaScript '
    // ' is a line-feed.

    // 多行字符串
    log(`In JavaScript this is
 not legal.`);
    // In JavaScript this is
    // not legal.

    log(`string text line 1
string text line 2`);
    // string text line 1
    // string text line 2

    $('#list').html(`
        <ul>
        <li>first</li>
        <li>second</li>
        </ul>
    `);
})(console.log, String)

4.2.2 使用反引号

代码中的模板字符串,都是用反引号表示。

如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

(function (log, S) {
    let greeting = `\`Yo\` World!`;
    log(greeting);// `Yo` World!
})(console.log, String)

4.2.3 使用变量

模板字符串中嵌入变量,需要将变量名写在${}之中。

(function (log, S) {
    // 字符串中嵌入变量
    let name = "Bob",
        time = "today";
    log(`Hello ${name}, how are you ${time}?`);
    // Hello Bob, how are you today?

    function authorize(user, action) {
        if (!user.hasPrivilege(action)) {
            throw new Error(
            // 传统写法为
            // 'User '
            // + user.name
            // + ' is not authorized to do '
            // + action
            // + '.'
            `User ${user.name} is not authorized to do ${action}.`);
        }
    }
})(console.log, String)

4.2.4 使用表达式

简单表达式

模板字符串大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性

(function (log, S) {
    let x = 1;
    let y = 2;

    log(`${x} + ${y} = ${x + y}`);
    // "1 + 2 = 3"

    log(`${x} + ${y * 2} = ${x + y * 2}`);
    // "1 + 4 = 5"

    let obj = {
        x: 1,
        y: 2
    };
    log(`${obj.x + obj.y}`);
    // "3"
})(console.log, String)

调用函数

模板字符串之中还能调用函数

(function (log, S) {
    function fn() {
        return "Hello World";
    }

    log(`foo ${fn()} bar`);
    // foo Hello World bar
})(console.log, String)

字符串变量

由于模板字符串大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出

(function (log, S) {
    let msg = `Hello ${'Melon'}`;
    log(msg);// Hello Melon
})(console.log, String)

需要时执行

如果需要引用模板字符串本身,在需要执行,可以像下面这样写。

(function (log, S) {
    // 写法一
    let str1 = 'return ' + '`Hello ${name}!`';
    let func1 = new Function('name', str1);
    log(func1('Melon')); // "Hello Melon!"

    // 写法二
    let str2 = '(name) => `Hello ${name}!`';
    let func2 = eval.call(null, str2);
    log(func2('Melon')); // "Hello Melon!"
})(console.log, String)

4.2.5 使用对象变量

如果大括号中的值不是字符串,将按照一般的规则转为字符串

比如,大括号中是一个对象,将默认调用对象toString方法。

(function (log, S) {
    let a = [1,3,4];
    let o = {melon:1};
    let d = Reflect.construct(Date,[]);

    log(`${a} + ${o} = ${d}`);
    // 1,3,4 + [object Object] = Mon Feb 11 2019 23:45:23 GMT+0800 (GMT+08:00)

})(console.log, String)

4.2.6 使用未声明变量

如果模板字符串中的变量没有声明,将报错

(function (log, S) {
    // 变量place没有声明
    let msg = `Hello, ${place}`;// ReferenceError: place is not defined
})(console.log, String)

4.2.7 模板嵌套

模板字符串可以嵌套

下面tmpl方法中,模板字符串变量之中,又嵌入了另一个模板字符串

(function (log, S) {
    const tmpl = addrs => `
    <table>
    ${addrs.map(addr => `
      <tr><td>${addr.first}</td></tr>
      <tr><td>${addr.last}</td></tr>
    `).join('')}
    </table>
  `;

    const data = [{
            first: '<Jane>',
            last: 'Bond'
        },
        {
            first: 'Lars',
            last: '<Croft>'
        },
    ];

    log(tmpl(data));
    // <table>
    //
    //   <tr><td><Jane></td></tr>
    //   <tr><td>Bond</td></tr>
    //
    //   <tr><td>Lars</td></tr>
    //   <tr><td><Croft></td></tr>
    //
    // </table>
})(console.log, String)

4.3 模板编译

4.3.1 准备需要转换的模板

const template = `
<ul>
  <% for(let i=0; i < data.supplies.length; i++) { %>
    <li><%= data.supplies[i] %></li>
  <% } %>
</ul>
`;

上面代码在模板字符串之中,放置了一个常规模板

该模板使用<%...%>放置 JavaScript 代码,使用<%= ... %>输出 JavaScript 表达式。


4.3.2 之前转换的方法

将其转换为 JavaScript 表达式字符串。

(function (log, S) {
    function tmpl(data) {
        const arr = [];
        arr.push('<ul>');
        for (let i = 0; i < data.supplies.length; i++) {
            arr.push('\n\t<li>');
            arr.push(data.supplies[i]);
            arr.push('</li>');
        };
        arr.push('\n</ul>');
        return arr.join('');
    }

    const data = {
        supplies: ["broom", "mop", "cleaner"]
    };
    log(tmpl(data));
    // <ul>
    //     <li>broom</li>
    //     <li>mop</li>
    //     <li>cleaner</li>
    // </ul>
})(console.log, String)

4.3.3 正则加echo和eval

使用正则表达式将模板中<%= ... %>转换为echo字符串拼接方法,然后再结合模板字符串,转译最后的编译方法完整体,最后再调用eval动态执行函数编译

(function (log, S) {
    function compile(template) {
        const evalExpr = /<%=(.+?)%>/g;
        const expr = /<%([\s\S]+?)%>/g;

        template = template.replace(evalExpr, '`); \n  echo( $1 ); \n  echo(`')

        template = template.replace(expr, '`); \n $1 \n  echo(`');

        template = 'echo(`' + template + '`);';

        let script =
            `(function parse(data){
          let output = "";

          function echo(html){
            output += html;
          }

          ${ template }

          return output;
        })`;

        return script;
    }

    const data = {
        supplies: ["broom", "mop", "cleaner"]
    };

    const temp = `
    <ul>
        <% for(let i=0; i < data.supplies.length; i++) { %>
            <li><%= data.supplies[i] %></li>
        <% } %>
    </ul>
    `;

    // let tmpl = new Function(temp,compile);
    const compileStr = compile(temp);
    log(compileStr);

    // (function parse(data) {
    //     let output = "";

    //     function echo(html) {
    //         output += html;
    //     }

    //     echo(`
    //         <ul>
    //             `);
    //     for (let i = 0; i < data.supplies.length; i++) {
    //         echo(`
    //                 <li>`);
    //         echo(data.supplies[i]);
    //         echo(`</li>
    //             `);
    //     }
    //     echo(`
    //         </ul>
    //         `);

    //     return output;
    // })

    let tmpl = eval(compileStr);
    log(tmpl(data));
    // <ul>
    //     <li>broom</li>
    //     <li>mop</li>
    //     <li>cleaner</li>
    // </ul>
})(console.log, String)

4.3.4 正则加数组和eval

使用正则表达式将模板中<%= ... %>转换为arr.push字符串拼接方法,然后再结合模板字符串,转译最后的编译方法完整体,最后再调用eval动态执行函数编译

(function (log, S) {
    function compile(template) {
        const evalExpr = /<%=(.+?)%>/g;
        const expr = /<%([\s\S]+?)%>/g;

        template = template.replace(evalExpr, '`); \n  arr.push( $1 ); \n  arr.push(`');
        // 替换模板字符串中的<%=...%>中的值  
        // 比如将 '<%= data.supplies[i] %>'
        // 转换为 '`);\n  arr.push( data.supplies[i]); \n  arr.push(`'

        template = template.replace(expr, '`); \n $1 \n  arr.push(`');
        // 替换模板字符串中的<%...%>中的值  
        // 比如将 '<% for(let i=0; i < data.supplies.length; i++) { %>'
        // 转换为 '`);\n  for(let i=0; i < data.supplies.length; i++)  \n  arr.push(`'

        template = 'arr.push(`' + template + '`);'; // 做最外层的包裹

        let script =
            `(function parse(data){
                const arr = [];

                ${ template }

                return arr.join('');
            })`;

        return script;
    }

    const data = {
        supplies: ["broom", "mop", "cleaner"]
    };

    const temp = `
    <ul>
        <% for(let i=0; i < data.supplies.length; i++) { %>
            <li><%= data.supplies[i] %></li>
        <% } %>
    </ul>
    `;

    // let tmpl = new Function(temp,compile);
    const compileStr = compile(temp);
    log(compileStr);

    // (function parse(data){
    //     const arr = [];

    //     arr.push(`
    //         <ul>
    //     `);
    //     for(let i=0; i < data.supplies.length; i++) {  
    //         arr.push(`
    //             <li>`);
    //         arr.push(  data.supplies[i]  );
    //         arr.push(`</li>
    //         `);
    //     }  
    //     arr.push(`
    //     </ul>
    // `);

    // return arr.join('');
    // })

    let tmpl = eval(compileStr);
    log(tmpl(data));
    // <ul>
    //     <li>broom</li>
    //     <li>mop</li>
    //     <li>cleaner</li>
    // </ul>
})(console.log, String)

4.4 模板字符串的限制

前面提到标签模板里面,可以内嵌其他语言

但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言

举例来说,标签模板里面可以嵌入 LaTEX 语言。


function latex(strings) {
  // ...
}

let document = latex`
\newcommand{\fun}{\textbf{Fun!}}  // 正常工作
\newcommand{\unicode}{\textbf{Unicode!}} // 报错
\newcommand{\xerxes}{\textbf{King!}} // 报错

Breve over the h goes \u{h}ere // 报错
`

上面代码中,变量document内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的。

但是 JavaScript 引擎会报错。原因就在于字符串转义

模板字符串会将\u00FF\u{42}当作 Unicode 字符进行转义,所以\unicode解析时报错;

\x56会被当作十六进制字符串转义,所以\xerxes会报错。

也就是说,\u\xLaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。

为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。

如果遇到不合法字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串


function tag(strs) {
  strs[0] === undefined
  strs.raw[0] === "\\unicode and \\u{55}";
}
tag`\unicode and \u{55}`

上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义限制,所以不报错了,JavaScript 引擎将第一个字符设置为undefined/

但是raw属性依然可以得到原始字符串,因此tag函数还是可以对原字符串进行处理。

注意,这种对字符串转义放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错


let bad = `bad escape sequence: \unicode`; // 报错

第五小节 字符串的遍历器接口

ES6字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of循环遍历。

(function (log, S) {
    for (let codePoint of 'foo') {
        log(codePoint)
    }
    // "f"
    // "o"
    // "o"
})(console.log, String)

除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF码点,传统的for循环无法识别这样的码点

(function (log, S) {
    const text = String.fromCodePoint(0x20BB7);

    for (let i = 0; i < text.length; i++) {
      log(text[i]);
    }
    // �
    // �

    for (let i of text) {
      log(i);
    }
    // "𠮷"
})(console.log, String)

上面代码中,字符串text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而for...of循环会正确识别出这一个字符


第六小节 标签模板

6.1.1 基础使用

模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为标签模板功能(tagged template)。

alert`123`
// 等同于
alert(123)

标签模板其实不是模板,而是函数调用的一种特殊形式标签指的就是函数,紧跟在后面的模板字符串就是它的参数


6.1.2 使用变量

但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数

let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

上面代码中,模板字符串前面有一个标识名tag,它是一个函数


6.1.3 返回值说明

整个表达式的返回值,就是tag函数处理模板字符串后的返回值

函数tag依次会接收到多个参数。


function tag(stringArr, value1, value2){
  // ...
}

// 等同于

function tag(stringArr, ...values){
  // ...
}

tag函数的第一个参数是一个数组,该数组成员模板字符串中那些没有变量替换的部分。

也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。


6.1.4 其他参数说明

tag函数的其他参数,都是模板字符串各个变量被替换后的

由于本例中,模板字符串含有两个变量,因此tag会接受到value1value2两个参数。

tag函数所有参数实际值如下。

参数顺序标识 参数值说明
第一个参数 ['Hello ', ' world ', '']
第二个参数 15
第三个参数 50

也就是说,tag函数实际上以下面的形式调用。

tag(['Hello ', ' world ', ''], 15, 50)

6.1.5 自定义使用

我们可以按照需要编写tag函数的代码。下面是tag函数的一种写法,以及运行结果。


let a = 5;
let b = 10;

function tag(s, v1, v2) {
  console.log(s[0]);
  console.log(s[1]);
  console.log(s[2]);
  console.log(v1);
  console.log(v2);

  return "OK";
}

tag`Hello ${ a + b } world ${ a * b}`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"

6.1.6 复杂使用

下面是一个更复杂的例子。


let total = 30;
let msg = passthru`The total is ${total} (${total*1.05} with tax)`;

function passthru(literals) {
  let result = '';
  let i = 0;

  while (i < literals.length) {
    result += literals[i++];
    if (i < arguments.length) {
      result += arguments[i];
    }
  }

  return result;
}

msg // "The total is 30 (31.5 with tax)"

上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。


6.1.7 结合rest参数

passthru函数采用 rest 参数的写法如下。


function passthru(literals, ...values) {
  let output = "";
  let index;
  for (index = 0; index < values.length; index++) {
    output += literals[index] + values[index];
  }

  output += literals[index]
  return output;
}

6.1.8 过滤 HTML 字符串

标签模板的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。


let message =
  SaferHTML`<p>${sender} has sent you a message.</p>`;

function SaferHTML(templateData) {
  let s = templateData[0];
  for (let i = 1; i < arguments.length; i++) {
    let arg = String(arguments[i]);

    // Escape special characters in the substitution.
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // Don't escape special characters in the template.
    s += templateData[i];
  }
  return s;
}

上面代码中,sender变量往往是用户提供的,经过SaferHTML函数处理,里面的特殊字符都会被转义。


let sender = '<script>alert("abc")</script>'; // 恶意代码
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;

message
// <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>

6.1.9 多语言转换

标签模板的另一个应用,就是多语言转换(国际化处理)。


i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`

// "欢迎访问xxx,您是第xxxx位访问者!"

模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断循环处理功能,但是通过标签函数,你可以自己添加这些功能。


// 下面的hashTemplate函数
// 是一个自定义的模板处理函数
let libraryHtml = hashTemplate`
  <ul>
    #for book in ${myBooks}
      <li><i>#{book.title}</i> by #{book.author}</li>
    #end
  </ul>
`;

6.1.10 嵌入其他语言

除此之外,你甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言


jsx`
  <div>
    <input
      ref='input'
      onChange='${this.handleChange}'
      defaultValue='${this.state.value}' />
      ${this.state.value}
   </div>
`

上面的代码通过jsx函数,将一个 DOM 字符串转为 React对象。你可以在 GitHub 找到jsx函数的具体实现。


6.1.11 运行Java代码

下面则是一个假想的例子,通过java函数,在 JavaScript 代码之中运行 Java 代码。

java`
class HelloWorldApp {
  public static void main(String[] args) {
    System.out.println("Hello World!"); // Display the string.
  }
}
`
HelloWorldApp.main();

6.1.12 第一个参数raw属性

模板处理函数的第一个参数模板字符串数组),还有一个raw属性。


console.log`123`
// ["123", raw: Array[1]]

上面代码中,console.log接受的参数,实际上是一个数组

数组有一个raw属性,保存的是转义后的原字符串

请看下面的例子。


tag`First line\nSecond line`

function tag(strings) {
  console.log(strings.raw[0]);
  // strings.raw[0] 为 "First line\\nSecond line"
  // 打印输出 "First line\nSecond line"
}

上面代码中,tag函数的第一个参数strings,有一个raw属性,也指向一个数组

数组成员strings数组完全一致。

比如,strings数组["First line\nSecond line"],那么strings.raw数组就是["First line\\nSecond line"]

两者唯一的区别,就是字符串里面的斜杠都被转义了。

比如,strings.raw 数组会将\n视为\\n两个字符,而不是换行符

这是为了方便取得转义之前的原始模板设计的。


mowatermelon commented 5 years ago

第五章 数值的扩展

进制表示优化

ES6提供了二进制和八进制数值的新写法,分别用前缀0b0o表示。


0b11111011 === 503 // true
0o767 === 503 // true

八进制用0o前缀表示的方法,将要取代已经在ES5中被逐步淘汰的加前缀0的写法。

原型方法扩展

ES6自Number对象上,新提供了Number.isFinite()和Number.isNaN(),两个方法,用来检查Infinite和NaN这两个特殊值。

与传统的isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,对于非数值一律返回false。

将全局方法parseInt()parseFloat(),移植到了Number对象上面,行为完全保持不变,这样做的目的,是逐步减少全局性方法,使语言逐步模块化。

Number.isInteger()用来判断一个值是否为整数,需要注意的是,在Javascript内部,整数和浮点数使用同样的存储方法,所以3和3.0被视为同一个值。

Number.isSafeInteger()则用来判断一个整数是否落在这个范围之内。

原型属性值

Javascript能够准确表示的整数范围为-2^53 ~ 2^53。ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Math

方法名 方法描述
Math.trunc() 用于去除一个数值的小数部分,返回其整数部分,注意这个方法不会做四舍五入,返回的是未修改数值整数部分。
Math.asinh(x) 返回x的反双曲正弦
Math.acosh(x) 返回x的反双曲余弦
Math.atanh(x) 返回x的反双曲正切
Math.sin(x) 返回x的双曲正弦
Math.cosh(x) 返回x的双曲余弦
Math.tanh(x) 返回x的双曲正切
Math.cbrt(x) 返回x的立方根
Math.clz32(x) 返回x的32位二进制整数表示形式的前导0的个数
Math.expml(x) 返回e^x -1
Math.found(x) 返回x的单精度浮点数形式
Math.hypot(...values) 返回所有参数的平方和的平方根
Math.imul(x,y) 返回两个参数以32位整数形式相乘的结果
Math.loglp(x) 返回 ln(1+x)
Math.log10(x) 返回 以10为底的对数,lg x
Math.log2(x) 返回 以2为底的对数,log2 x
mowatermelon commented 5 years ago

第六章 数组的扩展

原型对象方法扩展

方法名 方法描述
Array.form(arrLikeObj,dealItemFunc) 用于将两类对象转换为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象,其中包括ES6新增的Set和Map结构。
Array.of 用于将一组值转换为数组,主要是弥补数组构造函数Array()不足。因为参数个数的不同,会导致Array()的行为有差异,因为Array()的参数不少于2个,才会返回所提供参数组组成的新数组
Array.from(arrayLike,x => x % x);
// 等同于
Array.from(arrayLike).map(x => x % x);

Array.of(3,11,8) // [3,11,8]
Array.of(3).length // 1
Array(3).length // 3

实例对象方法扩展

方法名 方法描述
find(findFunc[,bindThis]) 用于找到第一个符合条件的数组元素。findFunc(value,index,arr)是一个回调函数,所有数组元素依次遍历该回调函数,直到找出第一个返回值为true的元素,然后返回该元素,反之返回 undefined,支持对于NaN的检测,从而弥补IndexOf()的不足
findIndex(findFunc[,bindThis]) 用于找到第一个符合条件的数组元素位置。findFunc(value,index,arr)是一个回调函数,所有数组元素依次遍历该回调函数,直到找出第一个返回值为true的元素,然后返回该元素位置,反之返回 -1,支持对于NaN的检测,从而弥补IndexOf()的不足
fill(fillNum[,fillStartNum,fillEndNum]) 给定值填充一个数组,用于空数组的初始化非常方便。如果未指定开始和结束索引,数组中已有的元素,会被全部抹去
[NaN].indexOf(NaN)// -1

[NaN].findIndex(x => Object.is(NaN,y))

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

数组遍历新增方法

ES6中每一个数组实例对象,都有entries(),keys()values()等三个新方法,都是返回的一个遍历器,可以用for...of循环进行遍历,entries()是对键值对的遍历,keys()是对键名的遍历和values()是对键值的遍历。

未知实现情况

数组推导

允许直接通过现有数组生成新数组,称为数组推导,for...of结构总是写在最前面,返回的表达式写在最后面,可以用于代替map方法。

for...of 后面还可以附加if语句,用来设定循环的限制条件,并且同时可以使用多个,可以用于代替 filter方法。

需要注意的是,数组推导的方括号构成一个单独的作用域,在这个方括号中声明的变量类似于使用let语句声明的变量。

由于字符串可以视为数组,因此字符串也可以直接用于数组推导。

关于数组推导需要注意的地方是,新数组会立即在内存中生成。这时如果原数组是一个很大的数组,将会非常耗费内存。


const a1 = [1,2];
const a2 = [for (i of a1) i * 2]; // [2,4]

const a1 = [1954,1974,1990,2006,2010,2014];
const a2 = [for (year of a1) if(year > 2000) year]; // [2006,2010,2014]
const a3 = [for (year of a1) if(year > 2000) if(year < 2010) year]; // [2006]

const a1 = [1,2];
const a2 = [10,20];
const a3 = [11,22];
const a4 = [for (a of a1) for(b of a2) for(c of a3) c-b-a]; // [0,0]

const s1 = [for (c of 'abcde' c+'0')].join('')// 'a0b0c0d0e0'

Array.observe() 和 Array.unobserve()

这两个方法用于监听(取消)监听数组的变化,指定回调函数,这个现在好像已经废弃了。

mowatermelon commented 5 years ago

第七章 对象的扩展

原型对象方法扩展

方法名 方法描述
Object.is(compareA,compareB) 用来比较两个值是否严格相等,它与严格比较运算符(===)的行为基本一致,不同之处有两点,一个是+0不等于-0,二是两个NaN相等。
Object.assign(targetObj,...sourceObj) 用于将源对象(source)的所有可枚举属性,复制都目标对象(target),第一个参数是目标对象,后面的参数都是源对象,只要有一个参数不是对象,就会抛出异常,如果目标对象与(多个)源对象有同名属性,则后面的属性会覆盖前面的属性
Object.setPrototypeOf(object,prototype) 作用与proto相同,用来设置一个对象的prototype对象
Object.getPrototypeOf(obj) 用来读取一个对象的prototype对象
Object.setPrototypeOf({},null);
Object.setPrototypeOf({});

语法糖

属性名缩写

ES6允许直接写入变量和函数,作为对象的属性和方法,这样的写法更加简洁。

这种写法用于函数的返回值,将会非常方便。

const Person = {
    name:'melon',
    birth,// 类似 birth:birth
    hello(){}// 类似 hello:function(){}
};

function getPoint(){
    const a = 1;
    const b = 2 ;
    return {a,b};
}
getPoint();// {a:1,b:2}

属性名表达式

ES6允许定义对象时,用表达式作为对象的属性名,在写法上,要把表达式放在方括号内。

const lastWord = 'last word';
const a = {
    'first word':'hello',
    [lastWord]:'melon'
};

const suffix = ' word';
const b = {
    ['first'+suffix]:'hello',
    ['last'+suffix]:'melon'
};

Symbol

ES6引入了一种新的原始数据类型Symbol,它通过Symbol函数生成,

Symbol函数接受一个字符串作为参数,用来指定生成的Symbol的名称,可以通过那么属性读取。typeof运算符的结果,表明Symbol是一种单独的数据类型。

注意,Symbol函数前不能使用new命令,否则会报错,这是因为生成的Symbol是一个原始类型的值,不是对象。

Symbol最大的特点,就是每一个Symbol都是不相等的,保证产生一个独一无二的值。所以Symbol类型适合作为标识符,用于对象的属性名,保证s属性名之间不发生冲突,如果一个对象由多个模块构成,这样就不会出现同名的属性。

Symbol类型作为属性名,可以被遍历,Object.getOwnPropertySymbols()和Object.getOwnPropertyKeys(),都可以获取该属性。

const mySymbol = Symbol('Test');

mySymbol.name // Test

typeof mySymbol // symbol

const w1 = Symbol();
const w2 = Symbol();
const w3 = Symbol();

// 普通写法
const mySymbol = Symbol();
const a = {
    [mySymbol]:'Hello'
};

// define写法
Object.defineProperty({},mySymbol,{value:'Hello'})

Proxy

可以理解为在目标对象之前,架设一层'拦截器',外界对该对象的访问,都需要先通过这层拦截,可以被过滤和改写。

可以用于异常属性获取时,及时抛出异常。


const proxy = new Proxy({},{
    get(target,property){
        if(property in target){
            return target[property];
        } else{
            throw new ReferenceError('blabla')
        }
    }
})
mowatermelon commented 5 years ago

第八章 函数的扩展

函数默认值

ES6 允许为函数的参数设置默认值,任何带有默认值的参数,都会被视为可选参数。不带默认值的参数视为必须参数

利用参数默认值,可以指定摸一个参数不得省略,如果省略就抛出一个错误。

function throwIfMissing(){
    throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()){
    return mustBeProvided;
}

foo();// Error:Missing parameter

rest 参数

ES6引入了rest参数(...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了,rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

数组特有的方法都可以用于这个变量。

注意,rest参数之后不能再有其他参数,否则会报错。

扩展运算符

扩展运算符(spread) 是三个点(...),它好比rest参数的逆运算,将一个数组转换为用逗号分隔的参数序列,该运算符主要用于函数调用。

扩展运算符,可以简化求出一个数组最大元素的写法。

扩展运算符可以简化数组和对象的赋值。

Math.max.apply(nul,[5,9,0]);

Math.max(...[5,9,0]);

const a1 = [1];
const a2 = [2];
const a3 = [3];
const a4 = [0,..a1,...a2,...a3];

const o1 = {a:1};
const o2 = {b:2};
const o3 = {c:3};
const o4 = {..o1,...o2,...o3};

箭头函数

ES6允许使用 箭头(=>)定义函数,可以用于简化回调函数。

如果箭头函数不需要参数或需要多个参数,就使用一对圆括号代表参数部分。

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

由于大括号被解释为代码块,因而如果箭头函数直接返回一个对象,必须在对象外面加上括号。

const getTempItem = id => ({id,name:'Temp’});

注意点

mowatermelon commented 5 years ago

第九章 Set和Map数据结构

Set

ES6提供新的数据结构Set(注意和原始数据类型的不同之处),它类似于数组,只不过它生成的值是用大括号包裹的,并且其成员数都是唯一的,没有重复的值,

Set本身时一个构造函数,用来生成Set数据结构。

Set函数可接受一个数组作为参数,用来初始化。

向Set加入值的时候,不会发生类型转换,这意味着,在Set中7和'7'时两个不同的值。

Set结构的对象实例化之后,有两个属性construcitorsize,前者用于返回当前实例对象的构造函数,默认就是Set函数。后者时返回Set成员总数。

实例方法名称 方法说明
add(value) 返回的值是当前已经添加过成员之后的Set对象,添加某个值到实例对象中,注意该方法可以存在链式调用
delete(value) 删除实例对象中某个值。返回一个布尔值,表示是否删除成功。
has(value) 返回一个布尔值,表示该值是否为Set的成员
clear() 清除所有成员

目前使用场景场景比较多的情况下是利用Set对象,做数组去重,先将数组值传递个Set构造函数,生成Set对象,自动去重,然后再通过Array.from再转换为数组对象进行返回。

((log) => {

    const items = new Set([2, 2, 2, 2, 2, 2, 2, 6]);
    log(items);//Set {2,6}
    log(items.add(3).add(9));// Set {2,6,3,9}

    log(items.has(2));// true
    log(items.has(3));// true
    log(items.delete(2));// true
    log(items.delete(4));// false
    log(items.has(2));// false
    log(items.clear());// undefined
    log(items);// Set {}
    log(items.size);// 0
})(console.log)

function dedupe(array){
    return Array.from(new Set(array));
}

Map

基础

Javascript的对象,本质上键值对的集合,但是只能用字符串当作键,这给它的使用带来了很大的限制。

如果在原始对象中将一个非字符串类型的值作为键名,则内部机制会自动隐式转换为一个字符串的值,类似于自动调用对象的toString方法,在作为对象存储值的存储键名。

为了解决这个问题,ES6提供了Map结构,它类似于对象,也是键值对的集合,但是键名的范围不限于字符串,对象也可以当作键名。

Map函数也可以接受一个二维数组进行数据初始化,默认数组中索引为0的为存储的键名,索引为1的为存储的键值,如果初始化的数据中,存在值和内存地址都相同的键名,则默认只保留最后一个的存储键值对。

Map结构的对象实例化之后,有两个属性construcitorsize,前者用于返回当前实例对象的构造函数,默认就是Map函数。后者时返回Map成员总数。

实例方法名称 方法说明
set(name,value) 返回的值是当前已经添加过成员之后的Map对象,添加某个值到实例对象中,注意该方法可以存在链式调用
get(name) 判断当前实例对象中是否含有某个键名为name的键值。如果在当前实例对象中能够正常取到对应的键值,则返回对应的键值,反之,则返回undefined
delete(value) 删除实例对象中某个值。返回一个布尔值,表示是否删除成功。
has(value) 返回一个布尔值,表示该值是否为Map的成员
clear() 清除所有成员,不返回任何值
((log) => {

    const m1 = new Map();
    log(m1);//Map {}
    log(m1.size);// 0
    const o = {22:'aa'};
    log(m1.set(o,'melon'));// Map {{22:'aa'} =>'melon'}
    log(m1);// Map {{22:'aa'} =>'melon'}
    log(m1.size);// 1
    log(m1.get(o));// melon
    log(m1.delete(o));// true

    const m2 = new Map([[11,22],[33,44]]);
    log(m2);// Map {11=>22,33=>44}
    log(m2.size);// 2
    log(m2.has(11));// true
    log(m2.get(11));// 22
    log(m2.has(33));// true
    log(m2.get(33));// 44
    log(m2.get(44));// undefined
    log(m2.clear());// undefined
    log(m2);// Map {}
    log(m2.size);// 0

    const m3 = new Map([[11,22],[11,44]]);
    log(m3);// Map {11=>44}
    log(m3.size);// 1

    const m4 = new Map([[[11],22],[[11],44]]);
    log(m4);// Map {[11]=>22,[11]=>44}
    log(m4.size);// 2
})(console.log)

对象遍历

Map对象提供三个遍历器

遍历器方法名 遍历器方法说明
keys() 返回键名的遍历器
values() 返回键值的遍历器
entries() 返回所有成员的遍历器
((log) => {

    const m1 = new Map([[11, 22], [33, 44]]);
    log(m1);//Map {11 =>22,33=>44}
    log(m1.size);// 2

    for (let key of m1.keys()) {
        log(`Key : ${key}`);
    }
    // Key : 11 
    // Key : 33

    for (let val of m1.values()) {
        log(`Value : ${val}`);
    }
    // Value : 22
    // Value : 44 

    for (let item of m1.entries()) {
        log(`Key : ${item[0]},Value : ${item[1]}`);
    }
    // Key : 11,Value : 22
    // Key : 33,Value : 44

    m1.forEach((value, key, map) => {
        log(`Key : ${key},Value : ${value}`);
    });
    // Key : 11,Value : 22
    // Key : 33,Value : 4

    const reporter = {
        report: function (key, value) {
            log(`Key : ${key},Value : ${value}`);
        }
    };
    // m1.forEach((value, key, map) => {
    //     this.report(key,value);
    // },reporter);
    //  这样指定this对象,再使用对应方法会报错
    //  注意不要使用箭头函数,这样第二个辅助参数挟持的this会失效,导致使用手动绑定对象方法会报错。
    m1.forEach(function(value, key, map) {
        this.report(key,value);
    },reporter);

    // Key : 11,Value : 22
    // Key : 33,Value : 44
})(console.log)
mowatermelon commented 5 years ago

第十章 Iterator和for...of循环

mowatermelon commented 5 years ago

第十一章 Generator 函数

mowatermelon commented 5 years ago

第十二章 Promise 对象

mowatermelon commented 5 years ago

第十三章 Class和Module

mowatermelon commented 5 years ago

总结

其实应该是在2019农历,过年前完成的,但是各种事,都到新的一年了,还有几章没有完成,日常捂脸.jpg。

自我反省

bad

开始接触es6有好几年了,但是一直没系统的学习,这边是在家弱网的情况下,开始系统的看,感觉阮大在14年就写的东西,我现在看,仍然可以看的一些新用法,就感觉就自己学习过于浮于表面的感觉。

而且当时是2014年,很多人对于前端都是觉得只是辅助的时候,当时是jq一梭哈的时候,这个时候在没有浏览器厂商支持的情况下,编写完这本书,感觉很厉害。

而且书中作序有提到,希望我们中国人声音能够被听到,能够参与到前端js的规范定制中,只有我们对于每个提案都更好的关注,有理有据的提出我们的建议,让国际知晓我们的声音。

2014年到现在2019年,五年过去了,ES7 ES8都已经有浏览器厂商实现部分功能了,但是感觉自己项目组中,也就2018年年末才开始有一些人开始了解es6,更多的是对js语法更替的忽视和抗拒,感觉环境也有些改变我了。

大前端口号喊了许久,而我还处于农村喜通网的状态,感觉要加快进度了。

good

当时阮大写书的时候,各大浏览器厂商都没怎么实现,现有目前很多功能都以及被实现。

当时babel规范还不太流行,我习惯了babel规范,再来看代码,会有一些觉得可以改进的地方。

在阅读前几章过程中,可以做到更少的代码描述和文字截取,因为已经有其他更好的关于这个知识点体系的思维逻辑,在这边只用来做梳理,甚至可以做相关纠错,这种感觉就有些赞。

在梳理文章的过程中,学习了一下阮大的分类习惯,按照自己的习惯重新分了一遍,自我感觉良好,日常捂脸.jpg。

生日礼物

算是送给自己的生日礼物吧

总要有一个坑 自己能够填完