Skip to main content

语言基础

3.2 关键字与保留字

break       do          in            typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try

3.3 变量

var声明提升

使用var时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:

function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined

这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部

3.3.2 let声明

作用域不同

letvar作用类似, 主要区别是let声明的范围是块作用域,而var声明的范围是函数作用域:

if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt

if (true) {
let age = 26;
console.log(age); // 26
}

声明提升 letvar的另一个重要的区别,就是let声明的变量不会在作用域中被提升, 而且在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError:

// name会被提升
console.log(name); // undefined
var name = 'Matt';

// age不会被提升
console.log(age); // ReferenceError:age is not defined
let age = 26;

全局声明 使用let在全局作用域中声明的变量不会成为window对象的属性,而var声明的变量则会。

for循环 for循环中, 使用var定义的迭代变量会渗透到循环体外部:

for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5

let就不会渗透到循环体外部.

上述导致的一个最常见的问题就是对迭代变量的奇特声明和修改:

for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 你可能以为会输出0、1、2、3、4
// 实际上会输出5、5、5、5、5

之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的i都是同一个变量,因而输出的都是同一个最终值。

说明JS里,闭包捕获的是变量引用,并不是变量的值。

而在使用let声明迭代变量时,JavaScript引擎在后台会为每个迭代循环声明一个新的迭代变量。每个setTimeout引用的都是不同的变量实例, 就不会有此问题。

3.3.3 const声明

JavaScript引擎会为for循环中的let声明分别创建独立的变量实例,虽然const变量跟let变量很相似,但是不能用const来声明迭代变量(因为迭代变量会自增):

for (const i = 0; i < 10; ++i) { 
// TypeError: invalid assignment to const 'i'
}

不过,如果你只想用const声明一个不会被修改的for循环变量,那也是可以的。也就是说,每次迭代只是创建一个新变量。这对for-of和for-in循环特别有意义:

for (const key in {a: 1, b: 2}) {
console.log(key);
}
// a, b

for (const value of [1,2,3,4,5]) {
console.log(value);
}
// 1, 2, 3, 4, 5

3.4 数据类型

ECMAScript有6种简单数据类型(也称为原始类型)

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Symbol (ECMAScript 6新增的)

一种复杂数据类型: Object(对象)

在ECMAScript中不能定义自己的数据类型,所有值都可以用上述7种数据类型之一来表示.

3.4.1 typeof操作符

对一个值使用typeof操作符会返回下列字符串之一:

  • "undefined": 表示值未定义;
  • "boolean": 表示值为布尔值;
  • "string": 表示值为字符串;
  • "number": 表示值为数值;
  • "object": 表示值为对象(而不是函数)或null;
  • "function": 表示值为函数;
  • "symbol": 表示值为符号。

注意typeof在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用typeof null返回的是 "object" 。这是因为特殊值null被认为是一个对空对象的引用。

注意 严格来讲,函数在ECMAScript中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过typeof操作符来区分函数和其他对象。

一般来说,永远不用显式地给某个变量设置undefined值。字面值undefined主要用于比较,而且在ECMA-262第3版之前是不存在的。增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。

3.4.4 Boolean类型

数据类型转换为true的值转换为false的值
Booleantruefalse
String非空字符串""(空字符串)
Number非零数值(包括无穷值)0、NaN(参见后面的相关内容)
Object任意对象
UndefinedN/A(不存在)undefined

3.4.5 Number类型

Number类型使用IEEE 754格式表示整数和浮点值(在某些语言中也叫双精度值).

八进制字面量,第一个数字必须是零(0),然后是相应的八进制数字(数值0~7).

十六进制字面量,必须让真正的数值前缀0x(区分大小写),然后是十六进制数字(0~9以及A~F)。十六进制数字中的字母大小写均可。

存储浮点值使用的内存空间是存储整数值的两倍,所以ECMAScript总是把值转换为整数。在小数点后面没有数字的或者小数点后面跟着的全部是0(比如 1.0),数值就会变成整数.

let floatNum1 = 1.;   // 小数点后面没有数字,当成整数1处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数10处理

浮点值的精确度最高可达17位小数,但在算术计算中远不如整数精确。例如,0.10.2得到的是0.300 000 000 000 000 04

之所以存在这种舍入错误,是因为使用了IEEE 754数值,其他使用相同格式的语言也有这个问题。

值的范围

ECMAScript可以表示的最小数值保存在Number.MIN_VALUE中,可以表示的最大数值保存在Number.MAX_VALUE中,如果某个计算得到的数值结果超出了该范围,那么这个数值会被自动转换为 -Infinity(负无穷大)或 Infinity(正无穷大).

Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY这两个属性包含的值分别就是 -InfinityInfinity.

可以使用isFinite()函数判断一个值是否是 Infinity.

NaN

NaN用于表示本来要返回数值的操作失败了(而不是抛出错误). 任何涉及NaN的操作始终返回NaN.

NaN不等于包括NaN在内的任何值.

isNaN()函数接收一个任意数据类型的参数,然后判断这个参数是否“不是数值”。isNaN()函数会尝试把传入的参数转换为数值(执行Number(val)进行数值转换), 任何不能转换为数值的值都会导致这个函数返回true:

isNaN(NaN);     // true
isNaN(10); // false,10是数值
isNaN("10"); // false,可以转换为数值10
isNaN("blue"); // true,不可以转换为数值
isNaN(""); // false, 因为 Number("") 返回 0
isNaN(true); // false,可以转换为数值1

数值转换

有3个函数可以将非数值转换为数值:Number()parseInt()parseFloat()Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。

Number()函数基于如下规则执行转换:

  • 布尔值,true转换为1,false转换为0。
  • 数值,直接返回。
  • null,返回0。
  • undefined,返回NaN。
  • 字符串,应用以下规则。
    • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number("1")返回1,Number("123")返回123,Number("011")返回11(忽略前面的零)。
    • 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
    • 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
    • 如果是空字符串(不包含字符),则返回0。
    • 如果字符串包含除上述情况之外的其他字符,则返回NaN。
  • 对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。

2、8、16进制之间转换

12345..toString(2) // 将10进制数字 12345 转成对应的二进制字符串
249..toString(8) // 将10进制数字 249 转成对应的八进制字符串
159..toString(16) // 将10进制数字 159 转成对应的十六进制字符串

parseInt("11000000111001", 2) // 将二进制数字字符串 11000000111001 格式化成10进制数字
parseInt("371", 8) // 将八进制数字字符串 371 格式化成10进制数字
parseInt("9f", 16) // 将十六进制数字字符串 9f 格式化成10进制数字

3.4.6 String类型

字符字面量

字面量含义
\n换行
\t制表
\b退格
\r回车
\f换页
\\反斜杠(\)
\'单引号('),在字符串以单引号标示时使用
\"双引号("),在字符串以双引号标示时使用
`反引号,在字符串以反引号标示时使用
\xnn以十六进制编码nn表示的字符(其中n是十六进制数字0~F),例如\x41等于"A"
\unnnn以十六进制编码nnnn表示的Unicode字符(其中n是十六进制数字0~F),例如\u03a3等于希腊字符"Σ"

转换为字符串

如果不确定一个值是不是nullundefined,可以使用String()转型函数,它始终会返回表示相应类型值的字符串.

字符串插值

模板字面量不是字符串,而是一种特殊的JavaScript句法表达式,在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。

任何JavaScript表达式都可以用于插值, 所有插入的值都会使用toString()强制转型为字符串.

let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!

模板字面量标签函数

模板字面量支持定义标签函数(tag function),通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。

function zipTag(strings, ...expressions) {
// strings 是一个数组: ["求和: ", " 加 ", " 等于 ", ""]
// strings.raw 是原始字符串数组, 和 String.raw 类似
// expressions[0]: 6
// expressions[1]: 9
// expressions[2]: 15
return strings[0] +
expressions.map((e, i) => `${e}${strings[i + 1]}`)
.join('');
}

const a = 6, b = 9;
const result = zipTag`求和: ${ a }${ b } 等于 ${ a + b }`; // "求和: 6 加 9 等于 15"

对于有n个插值的模板字面量,传给标签函数的表达式参数的个数始终是n,而传给标签函数的第一个参数所包含的字符串个数则始终是n+1。

原始字符串

console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9

3.4.7 Symbol类型

符号是原始值,且符号实例是唯一、不可变的。

符号的基本用法

符号需要使用Symbol()函数初始化:

const sym = Symbol();
console.log(typeof sym); // symbol

Symbol()函数不能与new关键字一起作为构造函数使用, 这样做是为了避免创建符号包装对象:

let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"

let myString = new String();
console.log(typeof myString); // "object"

let myNumber = new Number();
console.log(typeof myNumber); // "object"

new Symbol(); // TypeError: Symbol is not a constructor

如果你确实想使用符号包装对象,可以借用Object()函数:Object(Symbol())

使用全局符号注册表

Symbol(description: string)支持传入一个字符串参数作为对符号的描述(description),便于调试代码。但是,这个字符串参数与符号定义或标识完全无关。

Symbol.for(globalId: string)函数用一个字符串作为键,在全局符号注册表中创建并重用符号:

Symbol.for('foo') === Symbol.for('foo') // true

Symbol("foo") === Symbol("foo") // false , 注意 Symbol 函数的第一个字符串参数仅用于描述, 不是符号的唯一标识.
Symbol.for("foo") === Symbol("foo") // false, 每个 Symbol函数创建的实例都是唯一的

Symbol.keyFor(instance: Symbol)来查询全局符号(比如使用 Symbol.for(globalId: string) 创建的实例), 该方法接收符号实例, 返回该全局符号对应的字符串键. 如果查询的不是全局符号(比如使用 Symbol(desc: string)创建的实例), 则返回undefined.

使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号(包括对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性):

const s1 = Symbol("foo");
const obj = {
[s1]: "foo val"
};

Object.defineProperty(obj, Symbol("bar"), {value: 'bar val'});

符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键

Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。

bject.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键。

常用内置符号

ECMAScript 6也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol工厂函数字符串属性的形式存在。所有内置符号属性都是不可写、不可枚举、不可配置的。

Symbol.asyncIterator

ES2018规范定义

该符号作为一个属性表示 一个方法,该方法返回对象默认的AsyncIterator。由for-await-of语句使用, 换句话说,这个符号表示实现异步迭代器API的函数。

技术上,这个由Symbol.asyncIterator函数生成的对象应该通过其next()方法陆续返回Promise实例。可以通过显式地调用next()方法返回,也可以隐式地通过异步生成器函数返回:

class Emitter {
constructor(max) {
this.max = max;
this.asyncIdx = 0;
}

async *[Symbol.asyncIterator]() {
while(this.asyncIdx < this.max) {
yield new Promise((resolve) => resolve(this.asyncIdx++));
}
}
}

async function asyncCount() {
let emitter = new Emitter(5);

for await(const x of emitter) {
console.log(x);
}
}

asyncCount();
// 0
// 1
// 2
// 3
// 4
Symbol.hasInstance

该符号作为一个属性表示 一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof操作符使用:

function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
// 相当于执行了
console.log(Foo[Symbol.hasInstance](f)); // true

class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true
// 相当于执行了
console.log(Bar[Symbol.hasInstance](b)); // true

由于instanceof操作符会在原型链上寻找这个属性定义, 因此可以在继承的类上通过静态方法重新定义这个函数:

class Parent {}
class Child extends Parent {
static [Symbol.hasInstance]() {
return false;
}
}

let b = new Child();
console.log(b instanceof Parent); // true
console.log(b instanceof Child); // false
Symbol.isConcatSpreadable

该符号作为一个属性表示 一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素。 ES6中的Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcatSpreadable的值可以修改这个行为。

Symbol.iterator

该符合作为一个属性表示 一个方法,该方法返回对象默认的迭代器。由for-of语句使用