《ECMAScript 6 入门教程》学习笔记Ⅰ

news/2024/7/11 1:35:38 标签: js, javascript, es6, 前端, npm

ECMAScript 6 简介

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


js_5">Node.js

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

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

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

Babel 转码器

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子:

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

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

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

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

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

配置文件.babelrc

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

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

{
  "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 工具和模块的使用,都必须先写好.babelrc


命令行转码

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

babel-node

@babel/node 模块的 babel-node 命令,提供一个支持 ES6 的 REPL 环境。它支持 Node 的 REPL 环境的所有功能,而且可以直接运行 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

@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 命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码,所以只适合在开发环境使用。


babel API

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

javascript">var babel = require('@babel/core');

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

// 文件转码(异步)
babel.transformFile('filename.js', options, function(err, result) {
  result; // => { code, map, 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/。

下面是一个例子:

javascript">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 代码,第二个参数是转换的配置对象。


@babel/polyfill

Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如IteratorGeneratorSetMapProxyReflectSymbolPromise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。

举例来说,ES6 在 Array 对象上新增了 Array.from 方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用 babel-polyfill,为当前环境提供一个垫片。

安装命令如下:

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

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

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

Babel 默认不转码的 API 非常多,详细清单可以查看 babel-plugin-transform-runtime 模块的 definitions.js 文件。


浏览器环境

Babel 也可以用于浏览器环境,使用 @babel/standalone 模块提供的浏览器版本,将其插入网页。

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

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

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


let 和 const 命令

let 命令

基本用法

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

javascript">{
  let a = 10;
  var b = 1;
}

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

for循环的计数器,就很合适使用let命令:

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

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

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

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

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

上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

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

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

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

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

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

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

不存在变量提升

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

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

上面代码中,变量foo用var命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量bar用let命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

暂时性死区

有些“死区”比较隐蔽,不太容易发现:

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

bar(); // 报错

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

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

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

javascript">// 不报错
var x = x;

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

上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错“x 未定义”。

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

不允许重复声明


块级作用域

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

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

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

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

函数声明也是如此,严格模式下,函数只能声明在当前作用域的顶层。

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

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

const 命令

基本用法

const 声明一个只读的常量。一旦声明,常量的值就不能改变。

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

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

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

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

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

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

javascript">const foo = {};

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

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

上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

下面是另一个例子:

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

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

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

javascript">const foo = Object.freeze({});

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

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

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

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

ES6 声明变量的六种方法

var 命令、function 命令、let 命令、const命令、import 命令和 class 命令


顶层对象的属性

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

javascript">window.a = 1;
a // 1

a = 2;
window.a // 2

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

ES6 为了改变这一点,一方面规定,为了保持兼容性,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

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

let b = 1;
window.b // undefined

globalThis 对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。所以,很难找到一种方法,可以在所有情况下,都取到顶层对象。

下面是两种勉强可以使用的方法:

javascript">// 方法一
(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');
};

ES2020 在语言标准的层面,引入 globalThis 作为顶层对象。也就是说,任何环境下,globalThis 都是存在的,都可以从它拿到顶层对象,指向全局环境下的 this

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


变量的解构赋值

数组的解构赋值

基本用法

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

以前,为变量赋值,只能直接指定值。

javascript">let a = 1;
let b = 2;
let c = 3;

ES6 允许写成下面这样。

javascript">let [a, b, c] = [1, 2, 3];

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

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

javascript">let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

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

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

javascript">// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

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

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

javascript">let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

默认值

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

javascript">let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

javascript">let [x = 1] = [undefined];
x // 1

let [x = 1] = [null];
x // null

上面代码中,如果一个数组成员是 null,默认值就不会生效,因为null不严格等于 undefined


对象的解构赋值

简介

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

javascript">let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

javascript">let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

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

javascript">// 例一
let { log, sin, cos } = Math;

// 例二
const { log } = console;
log('hello') // hello

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

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

javascript">let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

注意,对象的解构赋值可以取到继承的属性。

javascript">const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);

const { foo } = obj1;
foo // "bar"

上面代码中,对象obj1的原型对象是obj2。foo属性不是obj1自身的属性,而是继承自obj2的属性,解构赋值可以取到这个属性。


字符串的解构赋值

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

javascript">const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

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

javascript">let {length : len} = 'hello';
len // 5

数值和布尔值的解构赋值

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

javascript">let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

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

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值,都会报错。

javascript">let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

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

javascript">function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

下面是另一个例子:

javascript">[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

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

javascript">function move({x = 0, y = 0} = {}) {
  return [x, y];
}

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

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

注意,下面的写法会得到不一样的结果。

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

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

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


圆括号问题

建议只要有可能,就不要在模式中放置圆括号。


用途

  1. 交换变量的值
javascript">let x = 1;
let y = 2;

[x, y] = [y, x];
  1. 从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

javascript">// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();
  1. 函数参数的定义

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

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

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

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

javascript">let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

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

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

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

  1. 函数参数的默认值
javascript">jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

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

  1. 遍历 Map 结构

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

javascript">const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

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

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

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

// 获取键值
for (let [,value] of map) {
  // ...
}
  1. 输入模块的指定方法

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

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

字符串的扩展

字符的 Unicode 表示法

ES6 加强了对 Unicode 的支持,允许采用 \uxxxx 形式表示一个字符,其中 xxxx 表示字符的 Unicode 码点。

javascript">"\u0061"
// "a"

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

javascript">"\uD842\uDFB7"
// "𠮷"

"\u20BB7"
// " 7"

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

ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。

javascript">"\u{20BB7}"
// "𠮷"

"\u{41}\u{42}\u{43}"
// "ABC"

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

'\u{1F680}' === '\uD83D\uDE80'
// true

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

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

javascript">'\z' === 'z'  // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

字符串的遍历器接口

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

javascript">for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"

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

javascript">let text = String.fromCodePoint(0x20BB7);

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

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

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

模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

javascript">// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

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

上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

javascript">let greeting = `\`Yo\` World!`;

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

javascript">$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

上面代码中,所有模板字符串的空格和换行,都是被保留的,比如<ul>标签前面会有一个换行。如果你不想要这个换行,可以使用 trim 方法消除它。

javascript">$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

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

javascript">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}.`);
  }
}

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

javascript">let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"

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

javascript">function fn() {
  return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar

如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的 toString 方法。

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

模板字符串甚至还能嵌套。


字符串的新增方法

String.fromCodePoint()

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

javascript">String.fromCodePoint(0x20BB7)
// "𠮷"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true

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

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


String.raw()

实例方法:codePointAt()

codePointAt() 方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与 charCodeAt() 方法相同。

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

javascript">let s = '𠮷a';

s.codePointAt(0) // 134071
s.codePointAt(1) // 57271

s.codePointAt(2) // 97

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

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

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

is32Bit("𠮷") // true
is32Bit("a") // false

实例方法:normalize()

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


实例方法:includes(), startsWith(), endsWith()

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

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

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

javascript">let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

上面代码表示,使用第二个参数 n 时,endsWith 的行为与其他两个方法有所不同。它针对前 n 个字符,而其他两个方法针对从第 n 个位置直到字符串结束。


实例方法:repeat()

repeat 方法返回一个新字符串,表示将原字符串重复 n 次。

javascript">'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

参数如果是小数,会被取整。

javascript">'na'.repeat(2.9) // "nana"

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

但是,如果参数是 0 到 -1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到 -1 之间的小数,取整以后等于-0repeat 视同为 0。

参数 NaN 等同于 0。


实例方法:padStart(),padEnd()

如果某个字符串不够指定长度,会在头部或尾部补全。padStart() 用于头部补全,padEnd() 用于尾部补全。

padStart()padEnd() 一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

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

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

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

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

另一个用途是提示字符串格式。

javascript">'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

实例方法:trimStart(),trimEnd()

ES2019 对字符串实例新增了 trimStart()trimEnd() 这两个方法。它们的行为与 trim() 一致,trimStart() 消除字符串头部的空格,trimEnd() 消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

浏览器还部署了额外的两个方法,trimLeft()trimStart() 的别名,trimRight()trimEnd() 的别名。


实例方法:matchAll()

matchAll() 方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。


正则的扩展

RegExp 构造函数

如果 RegExp 构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

javascript">new RegExp(/abc/ig, 'i').flags
// "i"

上面代码中,原有正则对象的修饰符是 ig,它会被第二个参数 i 覆盖。


字符串的正则方法

字符串对象共有 4 个方法,可以使用正则表达式:match()replace()search()split()

ES6 将这 4 个方法,在语言内部全部调用 RegExp 的实例方法,从而做到所有与正则相关的方法,全都定义在 RegExp 对象上。

String.prototype.match 调用 RegExp.prototype[Symbol.match]
String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
String.prototype.search 调用 RegExp.prototype[Symbol.search]
String.prototype.split 调用 RegExp.prototype[Symbol.split]


u 修饰符

一旦加上 u 修饰符号,就会修改下面这些正则表达式的行为:

点字符

点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于 0xFFFF 的 Unicode 字符,点字符不能识别,必须加上 u 修饰符。

Unicode 字符表示法

ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上 u 修饰符,才能识别当中的大括号,否则会被解读为量词

javascript">/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true

上面代码表示,如果不加 u 修饰符,正则表达式无法识别 \u{61} 这种表示法,只会认为这匹配 61 个连续的 u

量词

使用 u 修饰符后,所有量词都会正确识别码点大于 0xFFFF 的 Unicode 字符。

预定义模式

u 修饰符也影响到预定义模式,能否正确识别码点大于 0xFFFF 的 Unicode 字符。

javascript">/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷') // true

上面代码的 \S 是预定义模式,匹配所有非空白字符。只有加了 u 修饰符,它才能正确匹配码点大于 0xFFFF 的 Unicode 字符。

i 修饰符

有些 Unicode 字符的编码不同,但是字型很相近,比如,\u004B\u212A 都是大写的 K

javascript">/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

上面代码中,不加 u 修饰符,就无法识别非规范的 K 字符。

转义

没有 u 修饰符的情况下,正则中没有定义的转义(如逗号的转义 \,)无效,而在 u 模式会报错。


RegExp.prototype.unicode 属性

正则实例对象新增 unicode 属性,表示是否设置了 u 修饰符。

javascript">const r1 = /hello/;
const r2 = /hello/u;

r1.unicode // false
r2.unicode // true

上面代码中,正则表达式是否设置了 u 修饰符,可以从 unicode 属性看出来。


y 修饰符

除了 u 修饰符,ES6 还为正则表达式添加了 y 修饰符,叫做“粘连”(sticky)修饰符。

y 修饰符的作用与 g 修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g 修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。


RegExp.prototype.sticky 属性

y 修饰符相匹配,ES6 的正则实例对象多了 sticky 属性,表示是否设置了 y 修饰符。

javascript">var r = /hello\d/y;
r.sticky // true

RegExp.prototype.flags 属性

ES6 为正则表达式新增了 flags 属性,会返回正则表达式的修饰符。

javascript">// ES5 的 source 属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"

// ES6 的 flags 属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'ig'

s 修饰符:dotAll 模式

这被称为 dotAll 模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个 dotAll 属性,返回一个布尔值,表示该正则表达式是否处在 dotAll 模式。

/s 修饰符和多行修饰符 /m 不冲突,两者一起使用的情况下,. 匹配所有字符,而 ^$ 匹配每一行的行首和行尾。


后行断言

JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入后行断言,V8 引擎 4.9 版(Chrome 62)已经支持。


Unicode 属性类

ES2018 引入了一种新的类的写法 \p{...}\P{...},允许正则表达式匹配符合 Unicode 某种属性的所有字符。

具名组匹配

没看懂,以后再说;

正则匹配索引

正则匹配结果的开始位置和结束位置,目前获取并不是很方便。正则实例的 exex() 方法,返回结果有一个index属性,可以获取整个匹配结果的开始位置,但是如果包含组匹配,每个组匹配的开始位置,很难拿到。

现在有一个第三阶段提案,为exec()方法的返回结果加上indices属性,在这个属性上面可以拿到匹配的开始位置和结束位置。

String.prototype.matchAll()

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

ES2020 增加了 String.prototype.matchAll() 方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。

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

javascript">// 转为数组方法一
[...string.matchAll(regex)]

// 转为数组方法二
Array.from(string.matchAll(regex))

数值的扩展

二进制和八进制表示法

ES6 提供了二进制和八进制数值的新的写法,分别用前缀 0b(或 0B)和 0o(或 0O)表示。

javascript">0b111110111 === 503 // true
0o767 === 503 // true

Number.isFinite(), Number.isNaN()

ES6 在 Number 对象上,新提供了 Number.isFinite()Number.isNaN() 两个方法。

Number.isFinite() 用来检查一个数值是否为有限的(finite),即不是 Infinity

注意,如果参数类型不是数值,Number.isFinite 一律返回 false

Number.isNaN() 用来检查一个值是否为 NaN

如果参数类型不是 NaNNumber.isNaN 一律返回 false

它们与传统的全局方法 isFinite()isNaN() 的区别在于,传统方法先调用 Number() 将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite() 对于非数值一律返回 false, Number.isNaN() 只有对于 NaN 才返回 true,非 NaN 一律返回 false


Number.parseInt(), Number.parseFloat()

ES6 将全局方法 parseInt()parseFloat(),移植到 Number 对象上面,行为完全保持不变。

这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。


Number.isInteger()

Number.isInteger() 用来判断一个数值是否为整数。

JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。

如果参数不是数值,Number.isInteger 返回 false

注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger 可能会误判。

javascript">Number.isInteger(3.0000000000000002) // true

类似的情况还有,如果一个数值的绝对值小于 Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger 也会误判。

javascript">Number.isInteger(5E-324) // false
Number.isInteger(5E-325) // true

总之,如果对数据精度的要求较高,不建议使用 Number.isInteger() 判断一个数值是否为整数。


Number.EPSILON

ES6 在 Number 对象上面,新增一个极小的常量 Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。

对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的 1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方。

Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。

javascript">0.1 + 0.2
// 0.30000000000000004

0.1 + 0.2 - 0.3
// 5.551115123125783e-17

5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'

Number.EPSILON 可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即 Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。

javascript">5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2)
// true

因此,Number.EPSILON 的实质是一个可以接受的最小误差范围。

javascript">function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}

0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true

1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true

上面的代码为浮点数运算,部署了一个误差检查函数。


安全整数和 Number.isSafeInteger()

JavaScript 能够准确表示的整数范围在 -253 到 253之间(不含两个端点),超过这个范围,无法精确表示这个值。

ES6 引入了 Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER 这两个常量,用来表示这个范围的上下限。

Number.isSafeInteger() 则是用来判断一个整数是否落在这个范围之内。

这个函数的实现很简单,就是跟安全整数的两个边界值比较一下。

javascript">Number.isSafeInteger = function (n) {
  return (typeof n === 'number' &&
    Math.round(n) === n &&
    Number.MIN_SAFE_INTEGER <= n &&
    n <= Number.MAX_SAFE_INTEGER);
}

如果只验证运算结果是否为安全整数,很可能得到错误结果。下面的函数可以同时验证两个运算数和运算结果.

javascript">function trusty (left, right, result) {
  if (
    Number.isSafeInteger(left) &&
    Number.isSafeInteger(right) &&
    Number.isSafeInteger(result)
  ) {
    return result;
  }
  throw new RangeError('Operation cannot be trusted!');
}

trusty(9007199254740993, 990, 9007199254740993 - 990)
// RangeError: Operation cannot be trusted!

trusty(1, 2, 3)
// 3

Math 对象的扩展

ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。


Math.trunc()

Math.trunc 方法用于去除一个数的小数部分,返回整数部分

对于非数值,Math.trunc 内部使用 Number 方法将其先转为数值。

对于空值和无法截取整数的值,返回 NaN

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.trunc = Math.trunc || function(x) {
  return x < 0 ? Math.ceil(x) : Math.floor(x);
};

Math.sign()

Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

它会返回五种值。

  • 参数为正数,返回 +1
  • 参数为负数,返回 -1
  • 参数为 0,返回 0
  • 参数为-0,返回 -0;
  • 其他值,返回 NaN

如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回 NaN

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.sign = Math.sign || function(x) {
  x = +x; // convert to a number
  if (x === 0 || isNaN(x)) {
    return x;
  }
  return x > 0 ? 1 : -1;
};

Math.cbrt()

Math.cbrt 方法用于计算一个数的立方根。

对于非数值,Math.cbrt 方法内部也是先使用 Number 方法将其转为数值。

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.cbrt = Math.cbrt || function(x) {
  var y = Math.pow(Math.abs(x), 1/3);
  return x < 0 ? -y : y;
};

Math.clz32()

Math.clz32() 方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。

clz32 这个函数名就来自”count leading zero bits in 32-bit binary representation of a number“(计算一个数的 32 位二进制形式的前导 0 的个数)的缩写。

左移运算符(<<)与 Math.clz32 方法直接相关。

javascript">Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1 << 1) // 30
Math.clz32(1 << 2) // 29
Math.clz32(1 << 29) // 2

对于小数,Math.clz32 方法只考虑整数部分。

javascript">Math.clz32(3.2) // 30
Math.clz32(3.9) // 30

对于空值或其他类型的值,Math.clz32 方法会将它们先转为数值,然后再计算。


Math.imul()

Math.imul 方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。


Math.fround()

Math.fround 方法返回一个数的32位单精度浮点数形式。

Math.fround 方法的主要作用,是将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。

javascript">// 未丢失有效精度
Math.fround(1.125) // 1.125
Math.fround(7.25)  // 7.25

// 丢失精度
Math.fround(0.3)   // 0.30000001192092896
Math.fround(0.7)   // 0.699999988079071
Math.fround(1.0000000123) // 1

对于 NaNInfinity,此方法返回原值。对于其它类型的非数值,Math.fround 方法会先将其转为数值,再返回单精度浮点数。

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.fround = Math.fround || function (x) {
  return new Float32Array([x])[0];
};

Math.hypot()

Math.hypot 方法返回所有参数的平方和的平方根。

javascript">Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3);          // 3

上面代码中,3 的平方加上 4 的平方,等于 5 的平方。

如果参数不是数值,Math.hypot 方法会将其转为数值。只要有一个参数无法转为数值,就会返回 NaN


对数方法

ES6 新增了 4 个对数相关方法。

(1) Math.expm1()

Math.expm1(x) 返回 e^x - 1,即Math.exp(x) - 1

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.expm1 = Math.expm1 || function(x) {
  return Math.exp(x) - 1;
};

(2)Math.log1p()

Math.log1p(x) 方法返回1 + x 的自然对数,即 Math.log(1 + x)。如果 x 小于 -1,返回 NaN

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.log1p = Math.log1p || function(x) {
  return Math.log(1 + x);
};

(3)Math.log10()

Math.log10(x) 返回以 10 为底的 x 的对数。如果 x 小于 0,则返回 NaN

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.log10 = Math.log10 || function(x) {
  return Math.log(x) / Math.LN10;
};

(4)Math.log2()

Math.log2(x) 返回以 2 为底的 x 的对数。如果 x 小于 0,则返回 NaN

对于没有部署这个方法的环境,可以用下面的代码模拟。

javascript">Math.log2 = Math.log2 || function(x) {
  return Math.log(x) / Math.LN2;
};

双曲函数方法

ES6 新增了 6 个双曲函数方法。

  • Math.sinh(x) 返回 x 的双曲正弦(hyperbolic sine)
  • Math.cosh(x) 返回 x 的双曲余弦(hyperbolic cosine)
  • Math.tanh(x) 返回 x 的双曲正切(hyperbolic tangent)
  • Math.asinh(x) 返回 x 的反双曲正弦(inverse hyperbolic sine)
  • Math.acosh(x) 返回 x 的反双曲余弦(inverse hyperbolic cosine)
  • Math.atanh(x) 返回 x 的反双曲正切(inverse hyperbolic tangent)

指数运算符

ES2016 新增了一个指数运算符(**)。

javascript">2 ** 2 // 4
2 ** 3 // 8

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

javascript">let a = 1.5;
a **= 2;
// 等同于 a = a * a;

let b = 4;
b **= 3;
// 等同于 b = b * b * b;

BigInt 数据类型

简介

JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回 Infinity

ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

为了与 Number 类型区别,BigInt 类型的数据必须添加后缀 n

javascript">1234 // 普通整数
1234n // BigInt

// BigInt 的运算
1n + 2n // 3n

BigInt 同样可以使用各种进制表示,都要加上后缀 n

BigInt 与普通整数是两种值,它们之间并不相等。

typeof 运算符对于 BigInt 类型的数据返回 bigint

BigInt 可以使用负号(-),但是不能使用正号(+),因为会与 asm.js 冲突。


BigInt 对象

JavaScript 原生提供 BigInt 对象,可以用作构造函数生成 BigInt 类型的数值。转换规则基本与 Number() 一致,将其他类型的值转为 BigInt。

BigInt() 构造函数必须有参数,而且参数必须可以正常转为数值,下面的用法都会报错。

参数如果是小数,也会报错。

BigInt 对象继承了 Object 对象的两个实例方法。

  • BigInt.prototype.toString()
  • BigInt.prototype.valueOf()

它还继承了 Number 对象的一个实例方法。

  • BigInt.prototype.toLocaleString()

此外,还提供了三个静态方法。

  • BigInt.asUintN(width, BigInt): 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。
  • BigInt.asIntN(width, BigInt):给定的 BigInt 转为 -2width - 1 到 2width - 1 - 1 之间对应的值。
  • BigInt.parseInt(string[, radix]):近似于 Number.parseInt(),将一个字符串转换成指定进制的 BigInt。

转换规则

可以使用 Boolean()Number()String() 这三个方法,将 BigInt 可以转为布尔值、数值和字符串类型。

javascript">Boolean(0n) // false
Boolean(1n) // true
Number(1n)  // 1
String(1n)  // "1"

上面代码中,注意最后一个例子,转为字符串时后缀 n 会消失。

另外,取反运算符(!)也可以将 BigInt 转为布尔值。


数学运算

数学运算方面,BigInt 类型的 +-*** 这四个二元运算符,与 Number 类型的行为一致。除法运算 / 会舍去小数部分,返回一个整数。

javascript">9n / 5n
// 1n

几乎所有的数值运算符都可以用在 BigInt,但是有两个例外。

  • 不带符号的右移位运算符 >>>
  • 一元的求正运算符 +

上面两个运算符用在 BigInt 会报错。前者是因为 >>> 运算符是不带符号的,但是 BigInt 总是带有符号的,导致该运算无意义,完全等同于右移运算符 >>。后者是因为一元运算符 + 在 asm.js 里面总是返回 Number 类型,为了不破坏 asm.js 就规定 +1n会报错。

BigInt 不能与普通数值进行混合运算。

asm.js 里面,|0 跟在一个数值的后面会返回一个32位整数。根据不能与 Number 类型混合运算的规则,BigInt 如果与 |0 进行运算会报错。


http://www.niftyadmin.cn/n/1770233.html

相关文章

整理:控制台打印输出不同颜色字体工具类

使用方法&#xff1a;调用工具类里的showMe方法即可即可看到相应的颜色示例及调用代码&#xff1a; 代码如下&#xff1a; public class PRINT {public static void showMe() {PRINT.PN("灰色(PRINT.GREY)", PRINT.GREY);PRINT.PN("黑色(PRINT.BLACK)",…

python多线程执行_浅谈Python中的多线程

原标题&#xff1a;浅谈Python中的多线程 写在之前 在正式开始之前&#xff0c;先用比较通俗的语言给大家介绍几个比较重要的概念。首先是「并发编程」。「并发」其实在我们的生活中随处可见&#xff0c;比如我们去银行存钱&#xff0c;银行如果只有一个窗口并且办业务的人又比…

JavaScript去除数组中指定元素

从数组里移除某个指定元素&#xff1a; function deleteItem(arr,num) {let length arr.lengthfor (let i 0;i<length; i) {if (arr[i]num){arr.splice(i,1);i--}}return arr}let arr[1,2,2,3,4,2,2]console.log(deleteItem(arr,2))document.write("移除所有"2…

javaweb登录界面_Java Web轻松学45 - Maven登场

本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考&#xff0c;希望能帮助更多(Java)码农和想成为(Java)码农的人。目录介绍Maven是什么Maven下载Maven安装创建Maven工程/项目使用Maven构建工程/项目使用Maven消除添加依赖库的重复劳动总结介绍上篇…

《ECMAScript 6 入门教程》学习笔记 Ⅱ

函数的扩展 函数参数的默认值 指定了默认值以后&#xff0c;函数的 length 属性&#xff0c;将返回没有指定默认值的参数个数。也就是说&#xff0c;指定了默认值后&#xff0c;length 属性将失真。 (function (a) {}).length // 1 (function (a 5) {}).length // 0 (funct…

python property方法_python property方法的使用

property的作用&#xff1a;实例&#xff1a;class C:def __init__(self, size 10):self.size sizedef getSize(self):return self.sizedef setSize(self, value):self.size valuedef delSize(self):del self.sizex property(getSize, setSize, delSize)c C()print(c.getS…

js网页时钟(附素材抠图)

效果图如下&#xff1a; 代码 <!DOCTYPE html><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><title>电子时钟</title><style>.a{position:absolute;-webkit-transform:rotate(0deg)}</style>…

《ECMAScript 6 入门教程》学习笔记 Ⅲ

Reflect 概述 Reflect对象与Proxy对象一样&#xff0c;也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。 &#xff08;1&#xff09; 将Object对象的一些明显属于语言内部的方法&#xff08;比如Object.defineProperty&#xff09;&#xff0c;放到…