JavaScript高级程序设计(第4版)-第3章 语言基础

第3章 语言基础

注意EMCA-262是以一个名为ECMAScript的伪语言形式,定义了JavaScript的所有方面。

3.1 语法

ECMAScript语法借鉴了C语言和类C语言,有如下特性:

  • 区分大小写
  • 标识符:即变量、函数、属性、函数参数名称。标识符中的字母可以使是ASCII或Unicode中的字母字符,按照惯例采用驼峰命名法(因为ECMAScript的内置函数和对象都是驼峰命名法),命名规则有以下两点:
    • 首字符必须是字母、下划线_、美元$
    • 非首字符可以是字母、下划线_、美元$、数字
  • 注释:采用C语言风格的单行和块注释
  • 严格模式(strict mode):ES5时引入,主要目的是用来处理ES3的一些不规范写法,严格模式可以全局定义也可以局部定义,只需要在脚本开头(全局模式)或者作用代码区域开头(局部模式)添加一行预处理指令即可
<script>
    "use strict";
    //代码内容
</script>

<script>
    //非作用内容
    function fun(){
        "use strict";
        //作用内容
    }
</script>
  • 语句:ECMAScript语句以分号结尾,省略分号即默认由解析器确定语句结束位置。句末加分号有以下几点重要性:
    • 在压缩代码时防止出错
    • 省略解析器的识别任务,提升性能

3.2 关键字与保留字

ES6规定的关键字(用于特殊用途)如下:

也有一组未来保留字,用于给将来做关键词用的:

保留字不是不能用,但最好不要用,确保兼容过去和未来的ECMAScript版本

3.3 变量

ECMAScript属于松散类型变量,即变量可以用于保存任何类型的数据,每个变量只不过是一个用于保存任意值的命名占位符。

有3个关键字可以用于声明变量:

  • var
  • let(ES6-)
  • const(ES6-)

3.3.1 var关键字

使用var关键字,在不初始化的情况下,变量默认会存入一个特殊值undefined。

并且在声明变量后不强制规范变量的类型,即不仅可以改变变量的值,也能够改变变量的属性。

3.3.1.1 var声明作用域

使用var定义的变量会成为包含它的函数的局部变量。

但是在函数内定义变量时如果省略var关键字,创建出来就是一个全局变量(是的,可以省略var给函数直接初始化赋值,但是不推荐,因为很难维护,而且在strict mode下会报错)

function testA() { 
  var message = "testA"; //局部变量
}
function testB() { 
  message = "testB";  //全局变量
}
function testC() { 
  console.log(message);  //此处message已经被testB创建  output:testB
  message = "testC";
}
testA();
testB();
testC();
console.log(message);   //output:testC

 由于ECMAScript为松散型变量,因此也可以一次定义多个变量:

var messageA = "hello world",
    messageB = 1010,
    messageC = true;

注意:在strict mode下,不能定义名为eval和arguments的变量,否则会报错。

3.3.1.2 var声明提升

变量提升(hoist),也就是把所有变量声明都拉倒函数作用域的顶部,并且也可以反复多次的使用var来声明同一个变量,下面所展示的两段代码所具有的功能相同:

console.log(message); //output:undefined
var message = "hello";
var message = "world";
var message;
console.log(message);
message = "hello";
message = "world";

3.1.2 let声明

let和var最大的区别就在于作用域上。

  • var声明范围:函数作用域
  • let声明范围:块作用域

let也不允许同一个块作用域中出现冗余声明,但是嵌套块中使用相同的标识符不会报错

并且这种对冗余的检测不会因为var和let的混合声明而受影响,因为说白了,这两个关键字声明的不是不同的变量,区别只是指出变量在相关作用域如何存在

3.1.2.1 暂时性死区(temporal dead zone)

let没有声明提升,因此不能在声明变量前就引用变量,但是这不代表JavaScript引擎没有注意到let声明。在let声明之前的执行瞬间被称为暂时性死区。

3.1.2.2 全局声明

使用var声明的全局变量会成为window对象的属性,但是let并不会

3.1.2.3 条件声明

也许你会想到使用if或者是try/catch语句对let变量进行条件声明,但是这些方法都会因为let的块作用范围而变得无效,这并非是件坏事,因为条件声明是一种反模式,会让程序变得更难理解。

let a; 
if (typeof a === 'undefined') { 
  let a;  //这种方式创建的let变量被限制在if代码块的作用域内
}
try {
  console.log(a);
} catch (error) {
  let a;   //这种方式创建的let被限制在catch{}块的作用域内
 }

3.1.2.4 for循环中的let声明

由于var的作用域不局限于块,因此使用var定义for循环体中的迭代变量会渗透到循环体外,而let定义的作用域则仅限于循环体内,且JS引擎在后台会为每个迭代循环生命一个新的迭代变量,案例如下:

for(var i=0 ; i<5 ; i++){
    //output:5 5 5 5 5
    setTimeout(()=>{console.log(i)},10);
}
for(let i = 0; i<5 ;i++){
    //output:0 1 2 3 4
    setTimeout(()=>{console.log(i)},10);
}

3.3.3 const声明

使用const定义常量有以下几点需要注意:

  • 声明是必须同时初始化变量
  • const的作用域也是块
const name = "Matt";
if (true) {
  const name = "Nick";
}
console.log(name);  //output:Matt
  • const声明只适用于它指向的变量的引用,即对于const对象是不限制其属性的改变的
//const对象属性
const person = {};
person.name = "Suri";
  • const不能声明迭代变量,但是可以声明循环中不会被修改的变量
let index = 0;
for (const j = 0; index < 5; index++) { 
  //output:0 0 0 0 0 
  console.log(j);
}
//for-in遍历对象属性名
for (const key in { a: 10, b: 20 }) { 
  //output:10 20
  console.log(key);
}
//for-of遍历数组
for (const value of [10,20,30,40,50]) { 
  //output:10 20 30 40 50
  console.log(value);
}

3.3.4 声明风格及最佳实践

  1. 不使用var:有助于提升代码质量和更精确的声明作用域
  2. const优先,let次之:
    • 可以让浏览器运行时强制保持变量不变,防止因意外赋值导致的非预期行为
    • 让静态代码分析工具提前发现不合法的赋值操作

3.4 数据类型

ES6有6种简单数据类型(原始类型),1种复杂数据类型,如下:

  • 6种简单数据类型
    • Undefined:未定义
    • Null:空值
    • Boolean:布尔值
    • String:字符串
    • Number:数值
    • Symbol:符号
  • 1种复杂数据类型
    • Object:对象

3.4.1 typeof操作符

typeof操作符是针对ES变量定义松散型的特点,用于确定任意变量的数据类型的操作符,返回的值如下:

  • 'undefined':未定义
  • 'boolean':布尔值
  • 'string':字符串
  • 'number':数值
  • 'object':对象/null
  • 'function':函数
  • 'symbol':符号

使用typeof需要注意的地方:

  • typeof是一个操作符,而不是函数
  • typeof null = 'object',因为特殊值null被认为是一个空对象
  • ECMAScript认为函数是对象,但是函数在typeof返回值中有自己的特殊属性function,可以用typeof来区分function和object
  • typeof(未声明变量) = 'undefined',可以用于区分未声明和声明了的变量(前提是声明了的变量都初始化了)

3.4.2 Undefined类型

当使用var或let声明了变量但是没有初始化时,其值就为undefined。

增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。

对于值为undefined的变量,只能执行如下两个操作:

  • typeof
  • delete

undefined是一个假值(false),可以如下使用:

let flag;
//output:false
if (flag) {
  console.log(true);
} else { 
  console.log(false);
}

3.4.3 Null类型

null表示空对象指针。

建议使用null来对将来要保存对象值的变量进行初始化,这样就可以知道这个变量是否在后来被重新赋予了一个对象的引用。

undefined和null的关系有如下几点:

  • 在ECMA-262中,规定undefined和null是表面上相等的
console.log(undefined == null);  //output:true
  • 值为undefined的变量不用显式赋值,但是null的显式赋值是有实际用处的,可以用来保持空对象指针的语义,方便检测对象的引用。

null是一个假值(false)

let car = null;
//output:false
if (car) { 
  console.log(true);
} else {
  console.log(false);
}

3.4.4 Boolean类型

Boolean有两个字面量(区分大小写):

  • true
  • false

所有其他ECMAScript类型的值都有相应的布尔值的等价形式。

Boolean()转型函数可以将其他类型的值转换为布尔值,转换规则如下:

3.4.5 Number类型

Number类型使用IEEE754格式表示整数和浮点数。

以下是Number的几种数值字面量格式:

  • 十进制整数
  • 八进制:首字母为0/0o,然后是相应八进制数字(0-7),当字面两种包含数字超出了范围,则会忽略前缀0当做十进制数(注意严格模式下八进制前缀必须要用0o)
  • 十六进制:数值前缀0x(区分大小写),然后是十六进制数字(0-9,A-F,不区分大小写)

注意:由于JavaScript保存数值的方式,实际是存在+0和-0的区别的。

3.4.5.1 浮点值

浮点数格式:包含小数点,小数点前缀整数部分可省略,小数点后面数字不可省略

由于存储浮点值使用的存储空间是存储整数值的两倍,所以ECMAScript总是想方设法的把值转换为整数。特别是以下两种情形中:

  • 小数点后面没有数字
  • 小数点后面只跟着0

浮点值可以用科学计数法表示:【系数(整数/浮点数)】【E/e】【幂】,

ECMAScript将小数点后至少包含6个0的浮点值转换为科学计数法,

浮点数精度最高达17位小数,但由于使用了IEEE754数值标准,因此在浮点数值运算中存在微小的摄入错误,非常不建议使用浮点运算。

3.4.5.2 值的范围

ECMAScript有一些表示数值范围的特殊的值:

  • Number.MIN_VALUE:ES可以表示的最小值,大多是5E-324
  • Number.MAX_VALUE:ES可以表示的最大值,大多是1.7976931348623157E+308
  • Infinity:表示超出JS可以表示的范围,分为正无穷大Infinity和负无穷大-Infinity,次值没有可以用于计算的数值表示形式。
    • Number.NEGATIVE_INFINITY:负无穷大,-Infinity
    • Number.POSITIVE_INFINITY:正无穷大,Infinity
    • isFinite():用于判断一个值是不是有限大
    • 任何正数数/0=Infinity,任何负数/0=-Infinity

3.4.5.3 NaN(Not a Number)

用于表示本来要返回数值的操作失败了,有如下特性:

  • ±0/±0会得到NaN
  • 任何设计NaN的操作始终返回NaN
  • NaN == NaN =false
  • isNaN()判断接收的参数是否为数值,会将参数强制转换为数值类型。对于对象类型会先进性valueOf()方法,如果失败,再调用toString方法

3.4.5.4 数值转换

可以使用三个函数将非数值转换为数值类型:

  • Number():转型函数,作用于任何数据类型
  • parseInt():常用于字符串
  • parseFloat():常用于字符串
3.4.5.4.1 Number()

Number函数转换规则(和一元加操作符规则相同)比较复杂,如下所示:

  • Boolean:
    • true :1
    • false:0
  • Number:直接返回
  • null:0
  • undefined:NaN
  • String:
    • 包含数值字符串(0-9、±)转换为十进制数值
    • 包含浮点数值(m.n)转换为浮点数
    • 包含十六进制数值(0xn)转换为十六进制对应十进制整数
    • ""空字符串:0
    • 其他:NaN
  • Object:先调用valueOf()方法,失败(返回NaN)后调用toString()方法
3.4.5.4.2 parseInt()

parseInt()函数更专注字符串是否包含数值模式,有如下规则:

  • 字符串最前端空格省略,从第一个非空格字符开始转换
  • 如果第一个字符不是0-9,±,返回NaN
  • 空字符串:NaN
  • 能够识别不同的整数格式,但统一返回不同整数格式转换为十进制的格式
  • parseInt接受第二个参数,用于指定底数(进制数2/8/10/16),可以极大扩展转换后获得的结果类型,建议始终传给它第二个参数
3.4.5.4.3 parseFloat()

基本和parseInt相类似,额外的特性如下:

  • 只检测第一个出现的小数点
  • 始终忽略字符串开头的0
  • 十六进制数的返回值始终是0,parseFloat只解析十进制数,不能指定底数
  • 如果字符串表示的是整数,则parseFloat会返回整数

3.4.6 String类型

String表示0或多个16位Unicode字符序列,可以使用下面三种方式进行标识:

  • 双引号"
  • 单引号'
  • 反引号`

3.4.6.1 字符字面量

字符串字面量用于表示非打印字符或有其他用途的字符

使用转移序列表示的只算一个字符,可以使用字符串固有的length属性来检验:

let str = "Hello\tWorld!";
console.log(str); //output:Hello  World!
console.log(str.length);  //output:12

注意:如果字符串中包含双字节字符,则length属性返回的值可能不是准确的字符数

let str = "Hello\tWorld!";
console.log(str.length);  //output:12

3.4.6.2 字符串的特点

ECMAScript中的字符串是不可变的(immutable),

即要修改某个变量中的字符串值,必须先销毁原始字符串,再将包含新值的另一个字符串保存到改变量。

这也是一些早起浏览器在拼接字符串时很慢的原因,但浏览器后期的版本都针对性的解决了这个问题。

3.4.6.3 转换为字符串

一般有三种方式可以将一个非字符串值转换为字符串值:

  • toString()
  • String()
  • 用加号操作符给一个值加上一个空字符串""
3.4.6.3.1 toString()

几乎所有值都有toString()方法,toString的唯一作用就是返回当前值的字符串等价物。

toString()可以转换:

  • 数值
  • 布尔值
  • 对象
  • 字符串

不能转换:

  • null
  • undefined

多数情况,toString不接受参数,但是在对数值调用时,可以接受一个底数参数,用来规定以什么底数来输出数值的字符串表示,默认底数为10

let num = 10;
console.log(num.toString());  //10
console.log(num.toString(2)); //1010
console.log(num.toString(4)); //22
console.log(num.toString(8)); //12
console.log(num.toString(10));//10
console.log(num.toString(16));//a
3.4.6.3.2 String()

如果不确定一个值是不是null或者undefined,则可以调用String()转型函数,它始终会返回表示相应类型值的字符串,String()函数遵循如下规则:

  • 如果该值有toString方法,则直接调用toString方法
  • String(null) = 'null'
  • String(undefined) = 'undefined'

 

3.4.6.4 模板字面量

ES6新增的模板字面量能够保留换行字符,可以跨行定义字符串。

之所以取名为模板字面量,就是因为模板字面量在定义模板时特别有用

let template = `
<div id='app'>
  <h1>{{message}}</h1>
  <p>Hello,{{name}}</p>
<div> 
`

3.4.6.5 字符串插值

模板字面量最常用的特性就是支持字符串插值。

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

模板字面量中的字符串差值语法:${...}

let admin = {
  name: "Electric-Duck",
  lastLogin: 100,
  nowLogin:200,
};
let template = `
<div>
  <h1>Hello, ${admin.name}</h1>
  <p>You has LoginDown for ${ admin.nowLogin - admin.lastLogin} hours</p>
</div>
`
console.log(template);

所有插入值都会被toString强制转换为字符串

let admin = {
  name: "Electric-Duck",
  toString: function () { 
    return `Hello ${this.name}`;
  }
}
console.log(admin.name)
let template = `
<div>
  ${admin}
</div>
`
console.log(template);

3.4.6.6 模板字面量标签函数(tag function)

通过标签函数可以自定义插值行为。标签函数会接收被插值记号${}分隔后的模板和对每个表达式求值的结果。

tag function本身是一个常规的函数,通过前缀到模板字面量应用自定义行为。

使用剩余操作符(rest operation)...就可以接收可变数量的字符串插值参数,

对于有n个插值的模板字面量,传给标签函数的表达参数个数为n,第一个参数数组的长度为n+1

function simpleTag(strings, ...expressions) { 
  console.log(strings);   //['', ' + ', ' = ', '']
  for (const expression of expressions) { 
    console.log(expression);  //10 20 30
  }
  return strings[0] + expressions.map((e, i) => `${e}${strings[i+1]}`).join('');
}
let numA = 10, numB = 20;
let template = simpleTag`${numA} + ${numB} = ${numA + numB}`;   
console.log(template); //10 + 20 = 30

3.4.6.7 原始字符串String.raw

使用模板字面量也可以直接获取原始的模板字面量内容而非转换后的内容(Unicode字符),但是String.raw只对转移字符有效,对非转义字符形式的特殊字符是无效的:

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

也可以通过标签函数的第一个参数,即字符串数组的.raw属性获取每个字符串的原始内容:

function printRaw(strings) { 
  console.log("ActualStrings:");
  for (const string of strings) { 
    console.log(string);  //©  换行
  }
  console.log("RawStrings:")
  for (const string of strings.raw) { 
    console.log(string);  //\u00A9 \n
  }
}
printRaw`\u00A9${'and'}\n`;

3.4.7 Symbol类型

Symbol符号是ES6新增的数据类型,是原始值。

符号实例是唯一、不可变的。用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

符号并不是为了提供私有属性的行为才增加的,它就是用来创建唯一的记号,进而用作非字符串形式的对象属性。

3.4.7.1 符号的基本用法

符号的初始化函数:Symbol(description)

description参数为对符号的描述,可以用来调试代码,但是与符号定义或标识完全无关。

let genericSymbol = Symbol();
let othergenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol); //Symbol()
console.log(othergenericSymbol); //Symbol()
console.log(fooSymbol); //Symbol(foo)
console.log(otherFooSymbol) //Symbol(foo);
console.log(typeof genericSymbol)  //symbol
console.log(genericSymbol == othergenericSymbol);  //false
console.log(fooSymbol == otherFooSymbol); //false

Symbol没有字面量语法,只要创建Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性。

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

let myBoolean = new Boolean();
console.log(myBoolean);  //Boolean{false}
console.log(typeof myBoolean) //object

let myNumber = new Number();
console.log(myNumber);  //Number{0}
console.log(typeof myNumber); //object

let myString = new String();
console.log(myString);  //String{''}
console.log(typeof myString); //object

let mySymbol = new Symbol();  //TyoeError

如果想用符号包装对象,可以借用Object函数

let mySymbol = Symbol(); 
let myWrappedSymbol = Object(mySymbol);
console.log(myWrappedSymbol); //Symbol{Symbol(),description:undefined}
console.log(typeof myWrappedSymbol);    //object

3.4.7.2 使用全局符号注册表

在运行时不同部分需要共享和重用符号实例,可以用一个字符串作为key,在全局符号注册表中创建并重用符号。需要用到Symbol.for()方法

let fooGlobalSymbol = Symbol.for('foo');
console.log(fooGlobalSymbol); //Symbol(foo)
console.log(typeof fooGlobalSymbol) //symbol

Symbol.for对每个字符串建都执行幂等操作(同一个字符串实例化的Symbol同等),原理分两步:

  1. 第一次使用某个字符串调用时,检查全局运行时注册表,发现不存在对应符号,于是生成一个新富豪实例并添加到注册表中。
  2. 后续使用相同字符串调用同样检查注册表,发现存在与该字符串对应的符号,返回该符号实例。

使用Symbol.for全局符号注册表使用的description必须是字符串key,否则会被强行转换为字符串

console.log(Symbol.for("foo") == Symbol.for("foo")); //true
console.log(Symbol("foo") == Symbol.for("foo")); //false
console.log(Symbol.for()); //Symbol(undefined)

全局注册表的查询使用Symbol.keyFor(key)方法,接受符号(Symbol),返回对应的字符串键(description),当接受的参数是符号但不是全局符号时返回undefined,当接受的参数不是符号类型时会报错。

let symbolGlobal = Symbol.for('foo');
let symbolNormal = Symbol.for('foo')
console.log(Symbol.keyFor(symbolGlobal)); //foo
console.log(Symbol.keyFor(symbolNormal)); //undefined
console.log(Symbol.keyFor(symbol)); //ReferenceError

3.4.7.3 使用符号作为属性

凡是可以使用String或者Number作为属性的地方,都能够使用符号,包括几种属性的定义方法:

  • 初始化时直接定义
  • 直接定义
  • Object.defineProperty(对象名, 属性名, {value:值})
  • Object.defineProperties(对象名,{[属性名]:{value:值}, [属性名]:{value:值}, ...})
let symbolA = Symbol(`A`),
  symbolB = Symbol(`B`),
  symbolC = Symbol(`C`),
  symbolD = Symbol(`D`),
  symbolE = Symbol(`E`);
let obj = {
  [symbolA]:'symbol A',
}
console.log(obj); //{Symbol(A):'symbol A'}
obj[symbolB] = `symbol B`;
console.log(obj);  //{Symbol(A):'symbol A', Symbol(B):'symbol B'}
Object.defineProperty(obj, symbolC, { value: 'symbol C' });
console.log(obj); //{Symbol(A):'symbol A', Symbol(B):'symbol B', Symbol(C):'symbol C'}
Object.defineProperties(obj, {
  [symbolD]: { value: 'symbol D' },
  [symbolE]: { value: 'symbol E'}
})
console.log(obj);  //{Symbol(A):'symbol A', Symbol(B):'symbol B', Symbol(C):'symbol C',
                   // Symbol(D):'symbol D', Symbol(E):'symbol E'}

包含符号属性的对象在获取属性相关信息时分为四种不同的方式:

  • Object.getOwnPropertyNames():返回对象实例的常规属性数组
  • Object.getOwnPropertySymbols():返回对象实例的符号属性数组
  • Object.getOwnPropertyDescriptors():返回常规和符号属性描述符的对象
  • Reflect.ownKeys():返回两种类型的键
let sA = Symbol("A");
let sB = Symbol("B");
let obj = {
  [sA]: 'var A',
  [sB]: 123,
  nA: false,
  nB: 'Hello',
}
console.log(Object.getOwnPropertyNames(obj));  //['nA','nB']
console.log(Object.getOwnPropertySymbols(obj));  //[Symbol(A), Symbol(B)]
console.log(Object.getOwnPropertyDescriptors(obj)) //{ nA:{...}, nB:{...}, Symbol(A):{...}, Symbol(B):{...},}
console.log(Reflect.ownKeys(obj)); //['nA', 'nB', Symbol(A), Symbol(B)]

因为符号属性是对内存中符号的一个引用,直接创建并勇作属性的符号不会丢失,

但是如果没有显式的保存对这些属性的引用,就必须遍历对象的所有符号属性才能找到相应的属性键。

let obj = {
  [Symbol('A')]: 'symbol A',
  [Symbol('B')]: 'symbol B',
}
console.log(obj);   //{Symbol(A):'symbol A', Symbol(B):'symbol B'}
console.log(Object.getOwnPropertySymbols(obj).find((symbol)=>symbol.toString().match(/A/))); //Symbol(A)

3.4.7.4 常用内置符号

ES6引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为。

开发者可以直接访问、重写、模拟这些行为。

这些内置符号都已Symbol工厂函数字符串属性的形式存在。

内置符号最重要的用途之一就是重新定义它们,从而改变原生结构行为。

内置符号实际上也没什么特别的,就是全局函数Symbol的普通字符串属性,指向一个符号实例。

所有内置符号属性都是不可写、不可枚举、不可配置的。

ECMAScript规范常使用一种特殊的前缀方式@@来体积内置符号名称,

@@iterator指的就是Symbol.iterator

3.4.7.5 Symbol.asyncIterator(for-await-of)

类型:方法

该方法返回对象默认的AsyncIterator,由for-await-of语句使用

即该符号表示实现异步迭代器API的函数

for-await-of循环利用这个函数执行异步迭代操作,期望该函数返回一个实现迭代器API的对象

该方法大多数时候返回的对象是实现该API的AsyncGenerator

class Foo { 
  async *[Symbol.asyncIterator]() { }
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());    //AsyncGenerator{<suspended>}

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

class Emitter { 
  constructor(max) { 
    this.max = max;
    this.asyncIndex = 0;
  }
  async *[Symbol.asyncIterator]() {
    while (this.asyncIndex < this.max) { 
      yield new Promise(resolve => resolve(this.asyncIndex++));
    }
  }
}
async function asyncCount() { 
  let emitter = new Emitter(5);
  for await (const x of emitter) {  //for-await-of循环被重写了
    console.log(x);//0 1 2 3 4
  }
}
asyncCount();

3.4.7.6 Symbol.hasInstance(instanceof)

类型:方法

该方法决定一个构造器对象是否认可一个对象是它的实例。

由instanceof操作符使用。instanceof操作符可以用来确定一个对象实例的原型链上是否有原型

function Foo() { }
let f = new Foo();
console.log(f instanceof Foo); //true

class Bar { }
let b = new Bar();
console.log(b instanceof Bar); //true

instanceof操作符使用Symbol.hasInstance来确定关系

function Foo() { }
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f))

class Bar { }
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); //true

该属性顶一个Function原型上,默认所有的函数和类都能够调用。

因此可以在继承的类上通过静态方法重新定义该属性

class Bar { 
  static [Symbol.hasInstance]() { 
    return false;
  }
}
let b = new Bar();
console.log(b instanceof Bar);  //false

3.4.7.7 Symbol.isConcatSpreadable(Array.prototype.concat)

类型:布尔值

如果是true,意味着对象应该用Array.prototype.concat()打平其数组元素

ES6中的Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例,有以下几种对应选择:

  • 数组对象:
    • 默认情况:被打平到已有数组
    • Symbol.isConcatSpreadable=false:整个对象被追加到数组末尾
  • 类数组对象:
    • 默认情况:被追加到数组末尾
    • Symbol.isConcatSpreadable=true:类数组对象被打平到数组实例
  • 其他非数组对象:
    • Symbol.isConcatSpreadable=true时被忽略
let initial = ['foo'];
//数组对象
let array = ['bar'];  
console.log(array[Symbol.isConcatSpreadable]); //undefined
console.log(initial.concat(array)); //['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); //['foo', ['bar']]
//类数组对象
let arrayLikeObject = {
  length: 1,
  0:'baz',
}
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); //undefined
console.log(initial.concat(arrayLikeObject)); //['foo', {length:1, 0:'baz'}]
arrayLikeObject[Symbol.isConcatSpreadable] = 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;
console.log(initial.concat(otherObject)); //['foo']

3.4.7.8 Symbol.iterator(for-of)

类型:方法

该方法返回对象默认的迭代器,由for-of语句使用,表示实现迭代器API的函数

for-of循环时,会调用以Symbol.iterator为键的函数,并默认该函数返回一个实现迭代器API的对象

很多时候,返回的对象是实现该API的Generator

class Foo { 
  *[Symbol.iterator]() { }
}
let f = new Foo();
console.log(f[Symbol.iterator]());  //Generator{<suspended>}

技术上,由Symbol.iterator函数生成的对象应该通过其next()方法陆续返回值,

可以通过next显式返回,也可以通过生成器函数隐式返回

class Emitter{
  constructor(max){ 
    this.max = max;
    this.index = 0;
  }
  *[Symbol.iterator]() { 
    while (this.index < this.max) {
      yield this.index++;
    }
  }
}
let emitter = new Emitter(5);
for (const x of emitter) { 
  console.log(x);  //0 1 2 3 4
}

3.4.7.9 Symbol.match(String.prototype.match)

类型:正则表达式方法

该方法用正则表达式匹配字符串,由String.prototype.match()方法使用。该方法定义在正则表达原型上

console.log(RegExp.prototype[Symbol.match]);  //f[Symbol.match(){native code}]
console.log('foobar'.match(/bar/)); //['bar',index:3,input:'footbar',group:undefined]

Symbol.match方法具有如下特点:

  • 传入的非正则表达式值会导致该值被转换为RegExp对象
  • 可以重新定义Symbol.match以取代默认对正则表达式求值的行为,从而让match方法使用非正则表达式实例
  • Symbol.match接受一个参数,即调用match()方法的字符串实例
  • Symbol.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('foobar'.match(new StringMatcher('baz'))); //false

3.4.7.10 Symbol.replace(String.prototype.replace)

类型:正则表达式方法

该方法替换一个字符串中匹配的子串,由String.prototype.replace()方法使用,该方法默认定义在正则表达式的原型上

console.log(RegExp.prototype[Symbol.replace]);  //f[Symbol.replce](){[native code]}
console.log("foobarbaz".replace(/bar/, 'qux')); //fooquxbaz

Symbol.replace方法具有如下特点:

  • 传入的非正则表达式值会导致该值被转换为RegExp对象
  • 可以重新定义Symbol.replace以取代默认对正则表达式求值的行为,从而让replace方法使用非正则表达式实例
  • Symbol.replace接受2个参数,即调用replace()方法的字符串实例和替换字符串
  • Symbol.replace返回值没有限制
class FooReplacer { 
  static [Symbol.replace](target,replacement) {
    return target.split('bar').join(replacement);
   }
}
console.log("foobarbaz".replace(FooReplacer,'qux'));

class StringReplacer { 
  constructor(str) { 
    this.str = str;
  }
  [Symbol.replace](target,replacement) { 
    return target.split(this.str).join(replacement);
  }
}
console.log("foobarbaz".replace(new StringReplacer('bar'),'qux'))

3.4.7.11 Symbol.search(String.prototype.search)

类型:正则表达式方法

该方法返回字符串中匹配正则表达式的索引,由String.prototype.search()方法使用,定义在正则表达式原型上。

console.log(RegExp.prototype[Symbol.search]);  //f[Symbol.search](){[native code]}
console.log("foobar".search("bar"));  //3

Symbol.search方法具有如下特点:

  • 传入的非正则表达式值会导致该值被转换为RegExp对象
  • 可以重新定义Symbol.search以取代默认对正则表达式求值的行为,从而让search方法使用非正则表达式实例
  • Symbol.search接受1个参数,即调用search()方法的字符串实例
  • Symbol.search返回值没有限制
class FooSearcher { 
  static [Symbol.search](target) {
    return target.indexOf('foo');
   }
}
console.log('foobar'.search(FooSearcher));  //0
console.log('barfoo'.search(FooSearcher));  //3
console.log('quxbar'.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('quxbar'.search(new StringSearcher('foo'))) //-1

3.4.7.12 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

3.4.7.13 Symbol.split(String.prototype.split)

类型:正则表达式方法

该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用。定义在正则表达式原型上。

console.log(RegExp.prototype[Symbol.split]);  //f[Symbol.split](){[native code]}
console.log("foobarbaz".split(/bar/)); //['foo', 'baz']

Symbol.split方法具有如下特点:

  • 传入的非正则表达式值会导致该值被转换为RegExp对象
  • 可以重新定义Symbol.split以取代默认对正则表达式求值的行为,从而让splite方法使用非正则表达式实例
  • Symbol.split接受1个参数,即调用split()方法的字符串实例
  • Symbol.split返回值没有限制
class FooSplitter { 
  static [Symbol.split](target) { 
    return target.split('bar');
  }
}
console.log("foobarbaz".split(FooSplitter));  //['foo', 'baz']
class StringSplitter { 
  constructor(str) { 
    this.str = str;
  }
  [Symbol.split](target) { 
    return target.split(this.str);
  }
}
console.log("foobarbaz".split(new StringSplitter("bar")));  //['foo', 'baz']

3.4.7.14 Symbol.toPrimitive(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': return 'default bar';
       }
    }
  }
}
let bar = new Bar();
console.log(3 + bar); //3default bar
console.log(3 - bar); //0
console.log(String(bar)); //string bar

3.4.7.15 Symbol.toStringTag(Object.prototype.toString)

类型:字符串

该字符串用于创建对象的默认字符串描述,由内置方法Object.prototype.toString()使用

通过toString()方法来获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符默认Object。

内置类型已经指定了这个值,自定义类实例需要明确定义

let s = new Set();
console.log(s);  //Set(0)
console.log(s.toString()) //[obejct 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

3.4.7.16 Symbol.unscopables

类型:对象

该对象所有的以及继承的属性,都会从关联对象的with环境绑定中排除

设置这个符号并让其映射对应属性的键值为true,就可以组织该属性出现在with环境绑定中

(不推荐使用with,因此也不推荐使用Symbol,unscopables)

let obj = {
  foo:'bar'
}
with (obj) { 
  console.log(foo); //bar
}
obj[Symbol.unscopables] = {
  foo:true
}
with (obj) { 
  console.log(foo); //ReferenceError
}

3.4.8 Object类型

ECMAScript中的对象实际就是一组数据和功能的集合。

两种创建Object实例的方法:

let obj1 = new Object();
let obj2 = new Object;  //可以但不推荐

ECMAScript中的object实例本身不是很有用,但是派生其他对象的基类,每个object实例都有如下属性和方法:

  • constructor:构造函数
  • hasOwnProperty(propertyName):判断当前对象实例上是否存在给定的属性,属性必须是字符串或符号
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型
  • propertyIsEnumerable(PropertyName):用于判断给定的属性是否可以使用for-in语句枚举,属性必须是字符串或符号
  • toLocaleString():返回对象的字符串表示,该字符串反应对象所在的本地化执行环境
  • toString():返回对象的字符串表示
  • valueOf():返回对象对应的字符串、数值、布尔值表示。

注意:由于浏览器环境中的BOM和DOM对象都是由宿主环境定义和提供的宿主对象,而宿主对象是不受ECMA-262约束的,因此它们也可能不会继承Object。

3.5 操作符

3.5.1 一元操作符(unary operator)

只操作一个值的操作符即一元操作符

  • 递增/递减操作符
    • ++num/--num:前缀递增/递减操作符,先改变值,再参与计算
    • num++/num--:后缀递增/递减操作符,先参与计算,再改变值
    • 对于数值以外的数据类型:
      • 有效数值类型字符串:先变成数值再计算
      • 无效数值类型字符串:NaN
      • 布尔类型false:先变0,再计算
      • 布尔类型true:先变1再计算
      • 对象:先调用valueOf(),如果失败再调用toString
  • 一元加减
    • 一元加+num:和Number()转型函数转换规则一致
    • 一元减-num:先按一元加规则对操作数进行转换,再对中间结果取负获得最终结果

3.5.2 位操作符

  • 按位非:~num
  • 按位与:num1 & num2
  • 按位或:num1 | num2
  • 按位异或:num1 ^ num2
  • 左移:num<<移动位数
  • 有符号右移:num>>移动位数
  • 无符号右移:num>>>移动位数

3.5.3 布尔操作符

  • 逻辑非:!
    • 对于非布尔值,先进行强制转换,再取反
    • !!任意值就相当于调用了Boolean(),即双次取反
  • 逻辑与:&&(短路特性很重要)
    • 第一个操作数是对象,则返回第二个操作数
    • 第二个操作数是对象,则当第一个操作数为true时才返回该对象
    • 两个都是对象则返回第二个操作数
    • 有一个操作数是null,返回null
    • 有一个操作数是NaN,返回NaN
    • 有一个操作数是undefined,返回undefined
  • 逻辑或:||(同样有短路特性)
    • 第一个操作数是对象,返回第一个操作数
    • 第一个操作数求值为false,返回第二个操作数
    • 两个操作数都是null/NaN/undefined时,则返回null/NaN/undefined

3.5.4 乘性操作符

  • 乘法操作符 *
    • Infinity*0 = NaN
  • 除法操作符/
    • Infinity/Infinity = NaN
    • 0/0 = NaN
    • 非0/0 = ±Infinity
    • Infinity/任何数=±Infinity
  • 取模操作符%
    • 无限值%有限值=NaN
    • 有限值%0=NaN
    • Infinity%Infinity=NaN
    • 有限值%无限值=被除数
    • 0%非0=0

3.5.5 指数操作符

  • 系数**指数(Math.pow(系数, 指数))
  • 目标**=系数

3.5.6 加性操作符

  • 加法操作符 +
    • NaN+any=NaN
    • -Infinity+Infinity=NaN
    • -0+(+0)=+0
  • 减法操作符-
    • Infinity-Infinity=NaN
    • -Infinity-(-Infinity)=NaN
    • +0-(-0)=-0

3.5.7 关系操作符

  • 小于<
  • 大于>
  • 小于等于<=
  • 小于等于>=

注意:

  • 操作数都是字符串时,逐个比较字符串中对应字符的编码
  • 任何被强制数据转换成数值后得到的是NaN的数据和其他数据进行比较时结果都是false

3.5.8 相等操作符

  • ==,相等操作符,会先进行强制类型转换
  • !=,不相等操作符,会先进行强制类型转换
  • ===,全等操作符,比较时不转换操作数,推荐使用
  • !==,不全等操作符,比较时不转换操作数,推荐使用

注意:

  • null == undefined为true
  • num === undefined为false
  • NaN不等于任何数,NaN也不等于NaN

3.5.9 条件操作符

variable = boolean_expression ? true_value : false_value;

3.5.10 赋值操作符

简单赋值操作符即=

复合赋值操作符则是在原操作符右侧加上=,

num1 原操作符= num2

即:num1 = num1 原操作符 num2 

  • 乘后赋值:*=
  • 除后赋值/=
  • 取模后赋值%=
  • 加后赋值:+=
  • 减后赋值:-=
  • 左移后赋值:<<=
  • 右移后赋值:>>=
  • 无符号右移后赋值:>>>=

3.5.11 逗号操作符

在一条语句中同时声明多个变量是逗号操作符最常用的场景。

可以辅助赋值:

let num = (0, 1, 2, 3, 4, 5);
console.log(num); //5

3.6 语句

3.6.1 if语句

if (condition) statement1 else statement2 
//if-else if-else
if (condition1) statement1 
else if (condition2) statement2
else statment3

3.6.2 do-while语句

do{statement}while(expression)

特点:后测试循环语句,循环体内代码至少执行一次

3.6.3 while语句

whie(expression) statement

特点:先测试循环语句,循环体内代码可能不会执行

3.6.4 for语句

for(initialization; expression; post-loop-expression) statement

特点:先测试循环语句,初始化、条件表达式和循环后表达式都不是必须的,由于初始化定义的迭代器变量在循环执行完成后基本上不会再使用,因此最清晰的写法是使用let来声明迭代器变量,从而将这个变量的作用域限定在循环之中  

3.6.5 for-in语句

for (property in expression) statement

特点:

  • 是一种严格的迭代语句,用于枚举对象中的非符号键属性
  • 推荐使用const来定义property变量,确保局部变量不被修改
  • for-in不能保证返回对象属性的顺序,该返回顺序会因浏览器而异
  • for-in循环迭代变量如果是null或者undefined,则不会执行循环体

3.6.6 for-of语句

for (property of expression) statement

特点:

  • 是一种严格的迭代语句,用于遍历可迭代对象的元素
  • 推荐使用const声明property对象,确保局部变量不被修改
  • for-of循环按照可迭代对象的next()方法产生值的顺序迭代元素
  • 如果尝试迭代的变量不支持迭代,则会抛出错误
  • ES2018(ES9)扩展了for-await-of循环,以支持生成期约(promise)的异步可迭代对象

3.6.7 标签语句

label:statement

标签语句的典型应用场景是嵌套循环

3.6.8 break和continue语句

  • break:用于立即退出循环,强制执行循环后的下一条语句
  • continue:用于立即退出循环,但会再次从循环顶部开始执行

3.6.9 with语句

with(expression) statement

with语句作用是将代码作用域设置为特定对象。

应用场景是针对一个对象反复操作,这个时候将代码作用域设置为该对象能提供便利

with (location) { 
  console.log(search.substring(1));  //location.search.substring(1)
  console.log(hostname);  //location.hostname
  console.log(href);  //location.href
}

注意:在with内部,每个变量首先会被认为是一个局部变量,如果没有找到该局部变量,则会搜索expression对象,看它内部是否有同名属性,如果有则改变量会被求值为location对象的属性。

严格模式不允许使用with语句,并且with语句影响性能且难于调试其中代码,不推荐使用

 

3.6.10 switch语句

switch(expression){
    case value1:
         statement
         break;
    case value2:
         statement
         break;
    case value3:
         statement
         break;
    default:
         statement
}

注意:

  • switch语句在比较每个条件的值时使用的是全等操作符===,因此不会强制转换数据类型
  • 最后每个条件最后都加上break,除非确实需要连续匹配几个条件
  • switch条件可以是常量、变量或者表达式
let num = 15;
switch (true) { 
  case num < 10:
    console.log("num<10");
    break;
  case num >= 10 && num < 20:
    console.log("10≤num<20");  //output
    break;
  default:
    console.log("num≥20");
}

3.7函数

function functionName(arg0, arg1, ...argN){
    statements
}

ECMAScript函数不需要指定是否返回值,任何函数在任何时间都可以使用return语句来返回函数的值。只要碰到return语句,函数就会立刻停止执行并退出。

return语句也可以不带返回值,这种情况下,函数的返回值实际上为undefined

严格模式下函数的限制:

  • 函数不能以eval或者arguments作为名称
  • 函数的参数不能叫做eval或arguments
  • 两个命名参数不能拥有同一名称
posted @ 2022-06-27 11:28  Electric-Duck  阅读(49)  评论(0)    收藏  举报