语法基础(一)
# JavaScript 语法基础
# 严格模式
ECMAScript 5 增加了严格模式(strict mode)的概念。严格模式是一种不同的 JavaScript 解析和执行模型,ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。
# let 与条件声明
使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。
当使用 if
或者 try catch
语法进行条件声明时,使用 let 声明的变量将会被局限在块内。
为此,对于let 这个新的ES6 声明关键字,不能依赖条件声明模式。
# typeof 中的注意事项
typeof null
结果将会是 'object'- 严格来说,
function
不是对象,但是可以使用typeof
操作符区分function
与 其他对象
# 控制流最佳实践
控制流数据中条件判断建议使用 if (标志符)
# NaN 的含义
一个特殊的数值叫NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行。但在ECMAScript 中,0、+0 或0 相除会返回NaN:
# 使用标签函数改变模版字符串的默认行文
模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。 标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为,如下例所示。标签函数接收到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用 String.raw
方法可以避免模版字符串的转义
# Symbol 类型
用法
- 作为唯一标志符
- 使用
Symbol()
生成唯一符号 - 不能使用 new 关键字(为了避免创建包装对象)
- 创建 Symbol 的包装对象可以通过 Object 函数,
Object(Symbol())
# 使用全局符号注册表
使用 Symbol.for()
共享和重用同一个符号
let fooGlobalSymbol = Symbol.for('foo')
console.log(typeof fooGlobalSymbol) // symbol
let otherGlobalSymbol = Symbol.for('foo')
console.log(fooGlobalSymbol === otherGlobalSymbol) // true
2
3
4
即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不等同:
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
2
3
4
注册表中使用的键同时也会被用作符号描述。
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
2
传给 Symbol.keyFor() 的值不是 Symbol 时,将会抛出 TypeError
# 使用 Symbol 作为属性
- 可以使用显示赋值,也可以使用 Object.defineProperty / Object.defineProperties 来使用Symbol作为属性
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
};
// 这样也可以:o[s1] = 'foo val';
console.log(o);
// {Symbol(foo): foo val}
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val}
Object.defineProperties(o, {
[s3]: {value: 'baz val'},
[s4]: {value: 'qux val'}
});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val,
// Symbol(baz): baz val, Symbol(qux): qux val}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- 将会返回常规属性,类似于
Object.getOwnPropertyNames()
返回对象实例的常规属性数组,Object.getOwnPropertySymbols()
返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()
会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()
会返回两种类型的键。
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 如何没有显示保存 symbol 的属性名,只能通过过滤对应的属性值来寻找符号属性名了。
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o)
.find((symbol) => symbol.toString().match(/bar/));
console.log(barSymbol);
// Symbol(bar)
2
3
4
5
6
7
8
9
10
11
12
13
# Symbol 作为内置符号
ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol 工厂函数字符串属性的形式存在。 这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of 循环会在相关对象上使用 Symbol.iterator
属性,那么就可以通过在自定义对象上重新定义Symbol.iterator 的值,来改变for-of 在迭代该对象时的行为。 这些内置符号也没有什么特别之处,它们就是全局函数Symbol 的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
提示
注意 在提到ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是Symbol.iterator。
# Symbol.asyncIterator
根据ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator。由for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器API 的函数。 for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以Symbol.asyncIterator为键的函数,并期望这个函数会返回一个实现迭代器API 的对象。很多时候,返回的对象是实现该API的AsyncGenerator:
class Foo {
async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}
2
3
4
5
6
7
8
技术上,这个由 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
提示
注意 Symbol.asyncIterator 是 ES2018 规范定义的,因此只有版本非常新的浏览器 支持它。
# Symbol.hasInstance
作为对象的一个属性,使用对象的 obj[Symbol.hasInstance]
方法和 使用 instanceof 关键字是一个效果
同时,可以通过重新定义静态方法重写 Symbol.hasInstance
来改变 instanceof 的行为
class MyClass {
static [Symbol.hasInstance](instance) {
return instance instanceof Object;
}
}
let obj = { foo: 'bar' };
console.log(MyClass[Symbol.hasInstance](obj)); // true
2
3
4
5
6
7
8
# Symbol.isConcatSpreadable
这个符号作为一个属性表示“一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素”。 这个符号的主要目标就是允许用户定义的对象可以与Array.prototype.concat()很好地兼容。默认情况下,一个对象传递给concat()时,它会被简单地添加到数组中,作为一个元素:
let collection = {
0: 'foo',
1: 'bar',
length: 2,
[Symbol.isConcatSpreadable]: true
};
let items = ['baz'];
items = items.concat(collection);
console.log(items);
// ['baz', 'foo', 'bar']
2
3
4
5
6
7
8
9
10
11
# Symbol.iterator
这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of 语句使用”。这个符号表示实现迭代器API 的函数。 for-of 循环会在相关对象上使用这个函数,并期望这个函数会返回一个实现迭代器API 的对象。很多内置类型都实现了这个函数,比如 Array、Map 和 Set。自定义对象默认不实现这个函数,因此除非显式定义,否则它们不能被for-of 循环直接迭代。
class Emitter {
constructor(max) {
this.max = max;
this.idx = 0;
}
*[Symbol.iterator]() {
while(this.idx < this.max) {
yield this.idx++;
}
}
}
function count() {
let emitter = new Emitter(5);
for (const x of emitter) {
console.log(x);
}
}
count();
// 0
// 1
// 2
// 3
// 4
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Symbol.match
根据ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用”。String.prototype.matc()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String 方法的有效参数:
console.log(RegExp.prototype[Symbol.match]);
// ƒ [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/));
// ["bar", index: 3, input: "foobar", groups: unde
2
3
4
5
给这个方法传入非正则表达式值会导致该值被转换为RegExp 对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例。返回的值没有限制:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Symbol.replace
根据ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”。String.prototype.replace()方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String 方法的有效参数:
console.log(RegExp.prototype[Symbol.replace]);
// ƒ [Symbol.replace]() { [native code] }
console.log('foobarbaz'.replace(/bar/, 'qux'));
// 'fooquxbaz
2
3
4
5
给这个方法传入非正则表达式值会导致该值被转换为RegExp 对象。如果想改变这种行为,让方法直接使用参数,可以重新定义 Symbol.replace 函数以取代默认对正则表达式求值的行为,从而让replace()方法使用非正则表达式实例。Symbol.replace 函数接收两个参数,即调用replace()方法的字符串实例和替换字符串。返回的值没有限制:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Symbol.search
根据ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回一个字符串中匹配正则表达式的索引。由String.prototype.search()方法使用”。String.prototype.search()方法会使用以 Symbol.search 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String 方法的有效参数:
console.log(RegExp.prototype[Symbol.search]);
// ƒ [Symbol.search]() { [native code] }
console.log('foobar'.search(/bar/));
// 3
2
3
4
5
给这个方法传入非正则表达式值会导致该值被转换为RegExp 对象。如果想改变这种行为,让方法 直接使用参数,可以重新定义 Symbol.search 函数以取代默认对正则表达式求值的行为,从而让 search()方法使用非正则表达式实例。Symbol.search 函数接收一个参数,就是调用 match()方法 的字符串实例。返回的值没有限制:
class FooSearcher {
static [Symbol.search](target) {
return target.indexOf('foo');
}
}
console.log('foobar'.search(FooSearcher)); // 0
console.log('barfoo'.search(FooSearcher)); // 3
console.log('barbaz'.search(FooSearcher)); // -1
class StringSearcher {
constructor(str) {
this.str = str;
}
[Symbol.search](target) {
return target.indexOf(this.str);
}
}
console.log('foobar'.search(new StringSearcher('foo'))); // 0
console.log('barfoo'.search(new StringSearcher('foo'))); // 3
console.log('barbaz'.search(new StringSearcher('qux')));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Symbol.species
根据ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Symbol.split
根据ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split()方法使用”。String.prototype. split()方法会使用以 Symbol.split 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String 方法的有效参数:
console.log(RegExp.prototype[Symbol.split]);
// ƒ [Symbol.split]() { [native code] }
console.log('foobarbaz'.split(/bar/));
// ['foo', 'baz
2
3
4
5
给这个方法传入非正则表达式值会导致该值被转换为RegExp 对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.split 函数以取代默认对正则表达式求值的行为,从而让split()方法使用非正则表达式实例。Symbol.split 函数接收一个参数,就是调用 match()方法的字符串实例。返回的值没有限制:
class FooSplitter {
static [Symbol.split](target) {
return target.split('foo');
}
}
console.log('barfoobaz'.split(FooSplitter));
// ["bar", "baz"]
class StringSplitter {
constructor(str) {
this.str = str;
}
[Symbol.split](target) {
return target.split(this.str);
}
}
console.log('barfoobaz'.split(new StringSplitter('foo')));
// ["bar", "baz"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Symbol.toPrimitive
根据ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的Symbol.toPrimitive 属性上定义一个函数可以改变默认行为。
根据提供给这个函数的参数(string、number 或default),可以控制返回的原始值:
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"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Symbol.toStringTag
根据ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用”。 通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为 "Object"。内置类型已经指定了这个值,但自定义实例还需要明确定义。
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Symbol.unscopables
根据ECMAScript 规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with 环境绑定中排除”。设置止该属性出现在with 环境绑定中,如下例所示:
with (o) {
console.log(foo); // bar
}
o[Symbol.unscopables] = {
foo: true
};
with (o) {
console.log(foo); // ReferenceError
}
2
3
4
5
6
7
8
9
10
11
12
注意
不推荐使用with,因此也不推荐使用Symbol.unscopables。
# Object 类型
Object 类型的所有属性和方法在派生的对象上同样存在。
每个Object 实例都有如下属性和方法。
- constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object() 函数。
- hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如o.hasOwnProperty("name"))或符号。
- isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(第 8 章将详细介绍原型。)
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in 语句枚举。与hasOwnProperty()一样,属性名必须是字符串。
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行境。
- toString():返回对象的字符串表示。
- valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。
提示
注意 严格来讲,ECMA-262 中对象的行为不一定适合 JavaScript 中的其他对象。比如浏览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受ECMA-262 约束,所以它们可能会也可能不会继承Object。