第3章 语言基础
一、语法
-
ECMAScript 中一切都区分大小写。如
typeof是关键字,无法设置为变量名,但是Typeof可以设置为变量名。 -
标识符的要求是首字符需要为字母、下划线、美元符号,标识符的非首字符可以为字母、下划线、美元符号、数字。
-
标识符应采用驼峰大小写的形式,即第一个单词的首字母小写,后面每个单词的首字母大写,这跟 ECMAScript 内置函数和对象的命名方式一致。
-
ES5 增加了严格模式,规避了 ES3 的部分不规范写法。使用
"use strict";在脚本或函数体的首行可以开启严格模式。 -
省略分号意味着由解析器确定语句在哪里结尾,加分号的作用:
- 有助于防止省略造成的问题,避免输入内容不完整;
- 有助于删除空行以压缩代码行数的同时避免语法错误;
- 有助于提升性能,因为解析器会尝试补上分号以避免语法错误;
二、变量
- 变量是一个用于保存任意值的命名占位符,是松散类型的,这意味着变量可以保存任意类型的数据。有 3 个关键字可以声明变量:
var、const和let。 - 变量不初始化会保存一个特殊值
undefined。 - 使用
var定义会使变量成为包含它的函数的局部变量,该变量在函数退出时会被销毁。
function test() {
var message = 1; // 局部变量
}
test();
console.log(message); // Uncaught ReferenceError: a is not defined
- 在函数内定义变量省略
var,会使该变量成为全局变量。
function test() {
message = 1; // 全局变量
}
test();
console.log(message); // 1
- var声明的变量会自动提升到函数作用域的顶部,即把所有变量声明都拉到函数作用域的顶部。
function test() {
console.log(message);
var message = 1;
}
test(); // undefined
// 等同于
function test() {
var message;
console.log(message);
message = 1;
}
test(); // undefined
let和var的区别之一是,let声明的是块作用域,var声明的是函数作用域。块作用域是函数作用域的子集,因此适用于var的作用域限制同样也适用于let。
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
let不允许同一个块中出现冗余声明。嵌套使用相同的标识符不会报错,因为在同一个块中没有重复声明。
var name;
var name;
let age;
let age; // SyntaxError: 标识符age已经声明过了
- 声明冗余的报错不会受到
let和var混用的影响,这两个关键字只是指出变量在相关作用域中如何存在。 - 在
let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError。 for循环中var定义的迭代变量会渗透到循环体外部,let定义的迭代变量的作用域仅限于for循环块内部。
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 5 5 5 5 5
// 因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 0 1 2 3 4
// 使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。 每个 setTimeout 引用的都是不同的变量实例。
-
总结
let和var的区别:let声明的是块作用域,var声明的是函数作用域。let声明的变量不会在作用域中被提升。- 使用
let在全局作用域中声明的变量不会成为window对象的属性(var声明的变量则会)。
-
const和let的区别是用const声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。const声明的限制只适用于它指向的变量的引用。
// 用 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
-
声明风格及最佳实践:
- 不使用
var const优先,let次之
- 不使用
三、数据类型
-
ECMAScript 有六种简单数据类型,也称为原始类型:
Undefined、Null、Boolean、Number、String和Symbol。还有一种复杂数据类型叫Object(对象)。 -
调用
typeof null返回的是"object"。这是因为特殊值null被认为是一个对空对象的引用。 -
函数有自己特殊的属性。为此有必要通过
typeof操作符来区分函数和其他对象。 -
Undefined类型只有一个值,就是特殊值undefined。增加undefined的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。 -
Null类型同样只有一个值,即特殊值null,null值表示一个空对象指针。 -
任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用
null来填充该变量。 -
要将一个其他类型的值转换为布尔值,可以调用特定的
Boolean()转型函数,if等流控制语句会自动执行其他类型值到布尔值的转换。 -
存在
0.1+0.2≠0.3这种舍入错误,是因为使用了IEEE754数值,因此永远不要测试某个特定的浮点值。 -
任何涉及
NaN的操作始终返回NaN,NaN不等于包括NaN在内的任何值,任何不能转换为数值的值都会导致isNaN()函数返回true。 -
【补充】
Object.is(NaN, NaN)可以判断两个NaN相等。 -
有三个函数可以将非数值转换为数值:
Number()、parseInt()和parseFloat()。 -
使用
parseInt()转换空字符串会返回NaN,Number()会返回 0。 -
不传底数参数相当于让
parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给 它第二个参数。 -
因为
parseFloat()只解析十进制值,因此不能指定底数。 -
以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。
-
ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。
-
把一个值转换为字符串的方法有两种:
toString()和String()。 -
toString()方法可见于数值、布尔值、对象和字符串值,null和undefined值没有toString()方法。 -
如果不确定一个值是不是
null或undefined,可以使用String()转型函数。- 如果值有
toString()方法,则调用该方法(不传参数)并返回结果。 - 如果值是
null,返回"null"。 - 如果值是
undefined,返回"undefined"。
- 如果值有
-
模板字面量保留换行字符,可以跨行定义字符串。模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,只不过求值后得到的是字符串。字符串插值通过在
${}中使用一个 JavaScript 表达式实现。 -
字符串插值将表达式转换为字符串时会调用
toString():
let foo = { toString: () => 'World' };
console.log(`Hello, ${foo}!`); // Hello, World!
Symbol(符号)是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。Symbol()函数不能与new关键字一起作为构造函数使用,这是为了避免创建符号包装对象。- 如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。为此,需要使用
Symbol.for()方法。 - 全局注册表中的符号必须使用字符串键来创建,因此作为参数传给
Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。 Symbol.keyFor()可查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined。如果传给Symbol.keyFor()的不是符号,则该方法抛出TypeError。- 对象字面量只能在计算属性语法中使用符号作为属性。
- 常用内置符号用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。重新定义内置符号可以改变原生结构的行为。
Symbol.asyncIterator作为属性表示“一个方法,该方法返回对象默认的AsyncIterator。 由for-await-of语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。
class Foo {
async *[Symbol.asyncIterator]() { }
}
let foo = new Foo();
console.log(foo[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}
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 (let x of emitter) {
console.log(x);
}
}
asyncCount();
// 0 1 2 3 4
Symbol.hasInstance作为属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof操作符使用“。在 ES6 中,instanceof操作符会使用Symbol.hasInstance函数来确定关系。
class Bar{};
let bar = new Bar();
console.log(bar instanceof Bar); // true
console.log(Bar[Symbol.hasInstance](bar)); // true
class Baz extends Bar {
// 在继承的类上通过静态方法重新定义函数
static [Symbol.hasInstance]() {
return false;
}
}
let baz = new Baz();
console.log(baz instanceof Bar); // true
console.log(Bar[Symbol.hasInstance](baz)); // true
console.log(baz instanceof Baz); // false
console.log(Baz[Symbol.hasInstance](baz)); // false
Symbol.isConcatSpreadable作为属性表示“一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素”
let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
// 数组对象默认情况下会被打平到已有的数组
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
// false 或假值会导致整个对象被追加到数组末尾
console.log(initial.concat(array)); // ['foo', Array(1)]
let arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
// 类数组对象默认情况下会被追加到数组末尾
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
// true 或真值会导致这个类数组对象被打平到数组实例
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']
let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]
otherObject[Symbol.isConcatSpreadable] = true;
// true 的情况下不是类数组对象的对象将被忽略
console.log(initial.concat(otherObject)); // ['foo']
Symbol.iterator作为属性表示“一个方法,该方法返回对象默认的迭代器。 由for-of语句使用”,换句话说,这个符号表示实现迭代器 API 的函数。
class Foo {
*[Symbol.iterator]() { };
}
let f = new Foo();
console.log(f[Symbol.iterator]());
// Generator {<suspended>}
Symbol.match作为属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用”。Symbol.match函数接收一个参数,就是调用match()方法的字符串实例,返回的值没有限制。
console.log(RegExp.prototype[Symbol.match]);
// ƒ [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/));
// ["bar", index: 3, input: "foobar", groups: undefined]
class FooMatcher {
static [Symbol.match](target) {
return target.includes('foo');
}
}
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher {
constructor(str) {
this.str = str;
}
[Symbol.match](target) {
return target.includes(this.str);
}
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false
Symbol.replace作为属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由String.prototype.replace()方法使用”。
console.log(RegExp.prototype[Symbol.replace]);
// ƒ [Symbol.replace]() { [native code] }
console.log('foobarbaz'.replace(/bar/, 'qux'));
// 'fooquxbaz'
class FooReplacer {
static [Symbol.replace](target, replacement) {
return target.split('foo').join(replacement);
}
}
console.log('barfoobaz'.replace(FooReplacer, 'qux'));
// "barquxbaz"
class StringReplacer {
constructor(str) {
this.str = str;
}
[Symbol.replace](target, replacement) {
return target.split(this.str).join(replacement);
}
}
console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux'));
// "barquxbaz"
Symbol.search作为属性表示“表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()方法使用“。Symbol.species作为属性表示“一个函数值,该函数作为创建派生对象的构造函数”。用Symbol.species定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义。
class Bar extends Array { }
class Baz extends Array {
static get [Symbol.species]() {
return Array;
}
}
let bar = new Bar();
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
bar = bar.concat('bar');
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
let baz = new Baz();
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // true
baz = baz.concat('baz');
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // false
Symbol.split作为属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。Symbol.toPrimitive作为属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用”。通过在这个实例的Symbol.toPrimitive属性将对象转换为原始值。
class Foo { }
let foo = new Foo();
console.log(3 + foo); // "3[object Object]"
console.log(3 - foo); // NaN
console.log(String(foo)); // "[object Object]"
class Bar {
constructor() {
this[Symbol.toPrimitive] = function (hint) {
switch (hint) {
case 'number':
return 3;
case 'string':
return 'string bar';
case 'default':
default:
return 'default bar';
}
}
}
}
let bar = new Bar();
console.log(3 + bar); // "3default bar"
console.log(3 - bar); // 0
console.log(String(bar)); // "string bar"
Symbol.toStringTag作为属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用”。通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符。
let s = new Set();
console.log(s); // Set(0) {}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set
class Foo { }
let foo = new Foo();
console.log(foo); // Foo {}
console.log(foo.toString()); // [object object]
// 内置类型已经指定了这个值,但自定义类实例还需要明确定义
console.log(foo[Symbol.toStringTag]); // undefined
class Bar {
constructor() {
this[Symbol.toStringTag] = 'Bar';
}
}
let bar = new Bar();
console.log(bar); // Bar {}
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar
- ECMAScript 中的对象其实是一组数据和功能的集合,是派生其他对象的基类。
Object类型的所有属性和方法在派生的对象上同样存在。
四、操作符
- 在应用给对象时,操作符通常会调用
valueOf()和/或toString()方法来取得可以计算的值, - 后缀版与前缀版的主要区别在于,后缀版递增和递减在语句被求值后才发生。
- 同时使用两个叹号(
!!),相当于调用了转型函数Boolean()。 - 逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。
- 逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为
true,第二个操作数就不会再被求值了。利用这个行为,可以避免给变量赋值null或undefined - 加法操作符中,如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。
- 只要是数值和字符串比较,字符串就会先被转换为数值,然后进行数值比较。
- 等于和不等于在比较之前执行强制类型转换,全等和不全等在比较之前不执行类型转换。
- 在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值。
let num = (5, 1, 4, 8, 0); // num的值为0
五、语句
- 使用
let声明迭代器变量,这样就可以将这个变量的作用域限定在循环中。 for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。为了确保局部的迭代器变量不被修改,推荐使用const声明 。ECMAScript 中对象的属性是无序的,因此for-in语句不能保证返回对象属性的顺序。如果for-in循环要迭代的变量是null或undefined,则不执行循环体。for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素。为了确保局部的迭代器变量不被修改,推荐使用const声明。for-of循环会按照可迭代对象的next()方法产生值的顺序迭代元素。- 标签语句的典型应用场景是嵌套循环。
break语句用于立即退出循环,强制执行循环后的下一条语句。- 而
continue语句也用于立即退出循环,但会再次从循环顶部开始执行。 switch语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型。
六、函数
- 只要碰到
return语句,函数就会立即停止执行并退出。 - 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
- 不指定返回值的函数实际上会返回特殊值
undefined。