mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
mod
This commit is contained in:
290
极客时间专栏/重学前端/模块一:JavaScript/JavaScript对象:你知道全部的对象分类吗?.md
Normal file
290
极客时间专栏/重学前端/模块一:JavaScript/JavaScript对象:你知道全部的对象分类吗?.md
Normal file
@@ -0,0 +1,290 @@
|
||||
<audio id="audio" title="JavaScript对象:你知道全部的对象分类吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e6/a1/e6da746ef307617e41b7744d4564dea1.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
在前面的课程中,我已经讲解了JavaScript对象的一些基础知识。但是,我们所讲解的对象,只是特定的一部分,并不能涵盖全部的JavaScript对象。
|
||||
|
||||
比如说,我们不论怎样编写代码,都没法绕开Array,实现一个跟原生的数组行为一模一样的对象,这是由于原生数组的底层实现了一个自动随着下标变化的length属性。
|
||||
|
||||
并且,在浏览器环境中,我们也无法单纯依靠JavaScript代码实现div对象,只能靠document.createElement来创建。这也说明了JavaScript的对象机制并非简单的属性集合+原型。
|
||||
|
||||
我们日常工作中,接触到的主要API,几乎都是由今天所讲解的这些对象提供的。理解这些对象的性质,我们才能真正理解我们使用的API的一些特性。
|
||||
|
||||
## JavaScript中的对象分类
|
||||
|
||||
我们可以把对象分成几类。
|
||||
|
||||
<li>
|
||||
宿主对象(host Objects):由JavaScript宿主环境提供的对象,它们的行为完全由宿主环境决定。
|
||||
</li>
|
||||
<li>
|
||||
内置对象(Built-in Objects):由JavaScript语言提供的对象。
|
||||
<ul>
|
||||
- 固有对象(Intrinsic Objects ):由标准规定,随着JavaScript运行时创建而自动创建的对象实例。
|
||||
- 原生对象(Native Objects):可以由用户通过Array、RegExp等内置构造器或者特殊语法创建的对象。
|
||||
- 普通对象(Ordinary Objects):由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承。
|
||||
|
||||
下面我会为你一一讲解普通对象之外的对象类型。
|
||||
|
||||
### 宿主对象
|
||||
|
||||
首先我们来看看宿主对象。
|
||||
|
||||
JavaScript宿主对象千奇百怪,但是前端最熟悉的无疑是浏览器环境中的宿主了。
|
||||
|
||||
在浏览器环境中,我们都知道全局对象是window,window上又有很多属性,如document。
|
||||
|
||||
实际上,这个全局对象window上的属性,一部分来自JavaScript语言,一部分来自浏览器环境。
|
||||
|
||||
JavaScript标准中规定了全局对象属性,W3C的各种标准中规定了Window对象的其它属性。
|
||||
|
||||
宿主对象也分为固有的和用户可创建的两种,比如document.createElement就可以创建一些DOM对象。
|
||||
|
||||
宿主也会提供一些构造器,比如我们可以使用new Image来创建img元素,这些我们会在浏览器的API部分详细讲解。
|
||||
|
||||
## 内置对象·固有对象
|
||||
|
||||
我们在前面说过,固有对象是由标准规定,随着JavaScript运行时创建而自动创建的对象实例。
|
||||
|
||||
固有对象在任何JavaScript代码执行前就已经被创建出来了,它们通常扮演者类似基础库的角色。我们前面提到的“类”其实就是固有对象的一种。
|
||||
|
||||
ECMA标准为我们提供了一份固有对象表,里面含有150+个固有对象。你可以通过[这个链接](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-well-known-intrinsic-objects)查看。
|
||||
|
||||
但是遗憾的是,这个表格并不完整。所以在本篇的末尾,我设计了一个小实验(小实验:获取全部JavaScript固有对象),你可以自己尝试一下,数一数一共有多少个固有对象。
|
||||
|
||||
## 内置对象·原生对象
|
||||
|
||||
我们把JavaScript中,能够通过语言本身的构造器创建的对象称作原生对象。在JavaScript标准中,提供了30多个构造器。按照我的理解,按照不同应用场景,我把原生对象分成了以下几个种类。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/d0/6cb1df319bbc7c7f948acfdb9ffd99d0.png" alt="">
|
||||
|
||||
通过这些构造器,我们可以用new运算创建新的对象,所以我们把这些对象称作原生对象。<br>
|
||||
几乎所有这些构造器的能力都是无法用纯JavaScript代码实现的,它们也无法用class/extend语法来继承。
|
||||
|
||||
这些构造器创建的对象多数使用了私有字段,例如:
|
||||
|
||||
- Error: [[ErrorData]]
|
||||
- Boolean: [[BooleanData]]
|
||||
- Number: [[NumberData]]
|
||||
- Date: [[DateValue]]
|
||||
- RegExp: [[RegExpMatcher]]
|
||||
- Symbol: [[SymbolData]]
|
||||
- Map: [[MapData]]
|
||||
|
||||
这些字段使得原型继承方法无法正常工作,所以,我们可以认为,所有这些原生对象都是为了特定能力或者性能,而设计出来的“特权对象”。
|
||||
|
||||
## 用对象来模拟函数与构造器:函数对象与构造器对象
|
||||
|
||||
我在前面介绍了对象的一般分类,在JavaScript中,还有一个看待对象的不同视角,这就是用对象来模拟函数和构造器。
|
||||
|
||||
事实上,JavaScript为这一类对象预留了私有字段机制,并规定了抽象的函数对象与构造器对象的概念。
|
||||
|
||||
函数对象的定义是:具有[[call]]私有字段的对象,构造器对象的定义是:具有私有字段[[construct]]的对象。
|
||||
|
||||
JavaScript用对象模拟函数的设计代替了一般编程语言中的函数,它们可以像其它语言的函数一样被调用、传参。任何宿主只要提供了“具有[[call]]私有字段的对象”,就可以被 JavaScript 函数调用语法支持。
|
||||
|
||||
>
|
||||
[[call]]私有字段必须是一个引擎中定义的函数,需要接受this值和调用参数,并且会产生域的切换,这些内容,我将会在属性访问和执行过程两个章节详细讲述。
|
||||
|
||||
|
||||
我们可以这样说,任何对象只需要实现[[call]],它就是一个函数对象,可以去作为函数被调用。而如果它能实现[[construct]],它就是一个构造器对象,可以作为构造器被调用。
|
||||
|
||||
对于为JavaScript提供运行环境的程序员来说,只要字段符合,我们在上文中提到的宿主对象和内置对象(如Symbol函数)可以模拟函数和构造器。
|
||||
|
||||
当然了,用户用function关键字创建的函数必定同时是函数和构造器。不过,它们表现出来的行为效果却并不相同。
|
||||
|
||||
对于宿主和内置对象来说,它们实现[[call]](作为函数被调用)和[[construct]](作为构造器被调用)不总是一致的。比如内置对象 Date 在作为构造器调用时产生新的对象,作为函数时,则产生字符串,见以下代码:
|
||||
|
||||
```
|
||||
console.log(new Date); // 1
|
||||
console.log(Date())
|
||||
|
||||
```
|
||||
|
||||
而浏览器宿主环境中,提供的Image构造器,则根本不允许被作为函数调用。
|
||||
|
||||
```
|
||||
console.log(new Image);
|
||||
console.log(Image());//抛出错误
|
||||
|
||||
```
|
||||
|
||||
再比如基本类型(String、Number、Boolean),它们的构造器被当作函数调用,则产生类型转换的效果。
|
||||
|
||||
值得一提的是,在ES6之后 => 语法创建的函数仅仅是函数,它们无法被当作构造器使用,见以下代码:
|
||||
|
||||
```
|
||||
new (a => 0) // error
|
||||
|
||||
|
||||
```
|
||||
|
||||
对于用户使用 function 语法或者Function构造器创建的对象来说,[[call]]和[[construct]]行为总是相似的,它们执行同一段代码。
|
||||
|
||||
我们看一下示例。
|
||||
|
||||
```
|
||||
function f(){
|
||||
return 1;
|
||||
}
|
||||
var v = f(); //把f作为函数调用
|
||||
var o = new f(); //把f作为构造器调用
|
||||
|
||||
```
|
||||
|
||||
我们大致可以认为,它们[[construct]]的执行过程如下:
|
||||
|
||||
- 以 Object.prototype 为原型创建一个新对象;
|
||||
- 以新对象为 this,执行函数的[[call]];
|
||||
- 如果[[call]]的返回值是对象,那么,返回这个对象,否则返回第一步创建的新对象。
|
||||
|
||||
这样的规则造成了个有趣的现象,如果我们的构造器返回了一个新的对象,那么new创建的新对象就变成了一个构造函数之外完全无法访问的对象,这一定程度上可以实现“私有”。
|
||||
|
||||
```
|
||||
function cls(){
|
||||
this.a = 100;
|
||||
return {
|
||||
getValue:() => this.a
|
||||
}
|
||||
}
|
||||
var o = new cls;
|
||||
o.getValue(); //100
|
||||
//a在外面永远无法访问到
|
||||
|
||||
```
|
||||
|
||||
## 特殊行为的对象
|
||||
|
||||
除了上面介绍的对象之外,在固有对象和原生对象中,有一些对象的行为跟正常对象有很大区别。
|
||||
|
||||
它们常见的下标运算(就是使用中括号或者点来做属性访问)或者设置原型跟普通对象不同,这里我简单总结一下。
|
||||
|
||||
- Array:Array的length属性根据最大的下标自动发生变化。
|
||||
- Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。
|
||||
- String:为了支持下标运算,String的正整数属性访问会去字符串里查找。
|
||||
- Arguments:arguments的非负整数型下标属性跟对应的变量联动。
|
||||
- 模块的namespace对象:特殊的地方非常多,跟一般对象完全不一样,尽量只用于import吧。
|
||||
- 类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。
|
||||
- bind后的function:跟原来的函数相关联。
|
||||
|
||||
## 结语
|
||||
|
||||
在这篇文章中,我们介绍了一些不那么常规的对象,并且我还介绍了JavaScript中用对象来模拟函数和构造器的机制。
|
||||
|
||||
这是一些不那么有规律、不那么优雅的知识,而JavaScript正是通过这些对象,提供了很多基础的能力。
|
||||
|
||||
我们这次课程留一个挑战任务:不使用new运算符,尽可能找到获得对象的方法。
|
||||
|
||||
例子:
|
||||
|
||||
```
|
||||
var o = {}
|
||||
var o = function(){}
|
||||
|
||||
```
|
||||
|
||||
请把自己的答案留言给我,我们来比比看谁找到的多。
|
||||
|
||||
## 小实验:获取全部JavaScript固有对象
|
||||
|
||||
我们从JavaScript标准中可以找到全部的JavaScript对象定义。JavaScript语言规定了全局对象的属性。
|
||||
|
||||
三个值:
|
||||
|
||||
Infinity、NaN、undefined。
|
||||
|
||||
九个函数:
|
||||
|
||||
- eval
|
||||
- isFinite
|
||||
- isNaN
|
||||
- parseFloat
|
||||
- parseInt
|
||||
- decodeURI
|
||||
- decodeURIComponent
|
||||
- encodeURI
|
||||
- encodeURIComponent
|
||||
|
||||
一些构造器:<br>
|
||||
Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeakSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。
|
||||
|
||||
四个用于当作命名空间的对象:
|
||||
|
||||
- Atomics
|
||||
- JSON
|
||||
- Math
|
||||
- Reflect
|
||||
|
||||
我们使用广度优先搜索,查找这些对象所有的属性和Getter/Setter,就可以获得JavaScript中所有的固有对象。
|
||||
|
||||
请你试着先不看我的代码,在自己的浏览器中计算出来JavaScript有多少固有对象。
|
||||
|
||||
```
|
||||
var set = new Set();
|
||||
var objects = [
|
||||
eval,
|
||||
isFinite,
|
||||
isNaN,
|
||||
parseFloat,
|
||||
parseInt,
|
||||
decodeURI,
|
||||
decodeURIComponent,
|
||||
encodeURI,
|
||||
encodeURIComponent,
|
||||
Array,
|
||||
Date,
|
||||
RegExp,
|
||||
Promise,
|
||||
Proxy,
|
||||
Map,
|
||||
WeakMap,
|
||||
Set,
|
||||
WeakSet,
|
||||
Function,
|
||||
Boolean,
|
||||
String,
|
||||
Number,
|
||||
Symbol,
|
||||
Object,
|
||||
Error,
|
||||
EvalError,
|
||||
RangeError,
|
||||
ReferenceError,
|
||||
SyntaxError,
|
||||
TypeError,
|
||||
URIError,
|
||||
ArrayBuffer,
|
||||
SharedArrayBuffer,
|
||||
DataView,
|
||||
Float32Array,
|
||||
Float64Array,
|
||||
Int8Array,
|
||||
Int16Array,
|
||||
Int32Array,
|
||||
Uint8Array,
|
||||
Uint16Array,
|
||||
Uint32Array,
|
||||
Uint8ClampedArray,
|
||||
Atomics,
|
||||
JSON,
|
||||
Math,
|
||||
Reflect];
|
||||
objects.forEach(o => set.add(o));
|
||||
|
||||
for(var i = 0; i < objects.length; i++) {
|
||||
var o = objects[i]
|
||||
for(var p of Object.getOwnPropertyNames(o)) {
|
||||
var d = Object.getOwnPropertyDescriptor(o, p)
|
||||
if( (d.value !== null && typeof d.value === "object") || (typeof d.value === "function"))
|
||||
if(!set.has(d.value))
|
||||
set.add(d.value), objects.push(d.value);
|
||||
if( d.get )
|
||||
if(!set.has(d.get))
|
||||
set.add(d.get), objects.push(d.get);
|
||||
if( d.set )
|
||||
if(!set.has(d.set))
|
||||
set.add(d.set), objects.push(d.set);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
281
极客时间专栏/重学前端/模块一:JavaScript/JavaScript对象:我们真的需要模拟类吗?.md
Normal file
281
极客时间专栏/重学前端/模块一:JavaScript/JavaScript对象:我们真的需要模拟类吗?.md
Normal file
@@ -0,0 +1,281 @@
|
||||
<audio id="audio" title="JavaScript对象:我们真的需要模拟类吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d5/9d/d5fae4132d1af8608110297ffbe87e9d.mp3"></audio>
|
||||
|
||||
早期的JavaScript程序员一般都有过使用JavaScript“模拟面向对象”的经历。
|
||||
|
||||
在上一篇文章我们已经讲到,JavaScript本身就是面向对象的,它并不需要模拟,只是它实现面向对象的方式和主流的流派不太一样,所以才让很多人产生了误会。
|
||||
|
||||
那么,随着我们理解的思路继续深入,这些“模拟面向对象”,实际上做的事情就是“模拟基于类的面向对象”。
|
||||
|
||||
尽管我认为,“类”并非面向对象的全部,但我们不应该责备社区出现这样的方案,事实上,因为一些公司的政治原因,JavaScript推出之时,管理层就要求它去模仿Java。
|
||||
|
||||
所以,JavaScript创始人Brendan Eich在“原型运行时”的基础上引入了new、this等语言特性,使之“看起来语法更像Java”,而Java正是基于类的面向对象的代表语言之一。
|
||||
|
||||
但是JavaScript这样的半吊子模拟,缺少了继承等关键特性,导致大家试图对它进行修补,进而产生了种种互不相容的解决方案。
|
||||
|
||||
庆幸的是,从ES6开始,JavaScript提供了class关键字来定义类,尽管,这样的方案仍然是基于原型运行时系统的模拟,但是它修正了之前的一些常见的“坑”,统一了社区的方案,这对语言的发展有着非常大的好处。
|
||||
|
||||
实际上,我认为“基于类”并非面向对象的唯一形态,如果我们把视线从“类”移开,Brendan当年选择的原型系统,就是一个非常优秀的抽象对象的形式。
|
||||
|
||||
我们从头讲起。
|
||||
|
||||
## 什么是原型?
|
||||
|
||||
原型是顺应人类自然思维的产物。中文中有个成语叫做“照猫画虎”,这里的猫看起来就是虎的原型,所以,由此我们可以看出,用原型来描述对象的方法可以说是古已有之。
|
||||
|
||||
我们在上一节讲解面向对象的时候提到了:在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象。
|
||||
|
||||
最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java等流行的编程语言。这个流派叫做基于类的编程语言。
|
||||
|
||||
还有一种就是基于原型的编程语言,它们利用原型来描述对象。我们的JavaScript就是其中代表。
|
||||
|
||||
“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中,总是先有类,再从类去实例化一个对象。类与类之间又可能会形成继承、组合等关系。类又往往与语言的类型系统整合,形成一定编译时的能力。
|
||||
|
||||
与此相对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将它们分成类。
|
||||
|
||||
基于原型的面向对象系统通过“复制”的方式来创建新对象。一些语言的实现中,还允许复制一个空对象。这实际上就是创建一个全新的对象。
|
||||
|
||||
基于原型和基于类都能够满足基本的复用和抽象需求,但是适用的场景不太相同。
|
||||
|
||||
这就像专业人士可能喜欢在看到老虎的时候,喜欢用猫科豹属豹亚种来描述它,但是对一些不那么正式的场合,“大猫”可能更为接近直观的感受一些(插播一个冷知识:比起老虎来,美洲狮在历史上相当长时间都被划分为猫科猫属,所以性格也跟猫更相似,比较亲人)。
|
||||
|
||||
我们的JavaScript 并非第一个使用原型的语言,在它之前,self、kevo等语言已经开始使用原型来描述对象了。
|
||||
|
||||
事实上,Brendan更是曾透露过,他最初的构想是一个拥有基于原型的面向对象能力的scheme语言(但是函数式的部分是另外的故事,这篇文章里,我暂时不做详细讲述)。
|
||||
|
||||
在JavaScript之前,原型系统就更多与高动态性语言配合,并且多数基于原型的语言提倡运行时的原型修改,我想,这应该是Brendan选择原型系统很重要的理由。
|
||||
|
||||
原型系统的“复制操作”有两种实现思路:
|
||||
|
||||
<li>
|
||||
一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用;
|
||||
</li>
|
||||
<li>
|
||||
另一个是切实地复制对象,从此两个对象再无关联。
|
||||
</li>
|
||||
|
||||
历史上的基于原型语言因此产生了两个流派,显然,JavaScript显然选择了前一种方式。
|
||||
|
||||
## JavaScript的原型
|
||||
|
||||
如果我们抛开JavaScript用于模拟Java类的复杂语法设施(如new、Function Object、函数的prototype属性等),原型系统可以说相当简单,我可以用两条概括:
|
||||
|
||||
- 如果所有对象都有私有字段[[prototype]],就是对象的原型;
|
||||
- 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
|
||||
|
||||
这个模型在ES的各个历史版本中并没有很大改变,但从 ES6 以来,JavaScript提供了一系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:
|
||||
|
||||
- Object.create 根据指定的原型创建新对象,原型可以是null;
|
||||
- Object.getPrototypeOf 获得一个对象的原型;
|
||||
- Object.setPrototypeOf 设置一个对象的原型。
|
||||
|
||||
利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。我用下面的代码展示了用原型来抽象猫和虎的例子。
|
||||
|
||||
```
|
||||
var cat = {
|
||||
say(){
|
||||
console.log("meow~");
|
||||
},
|
||||
jump(){
|
||||
console.log("jump");
|
||||
}
|
||||
}
|
||||
|
||||
var tiger = Object.create(cat, {
|
||||
say:{
|
||||
writable:true,
|
||||
configurable:true,
|
||||
enumerable:true,
|
||||
value:function(){
|
||||
console.log("roar!");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
var anotherCat = Object.create(cat);
|
||||
|
||||
anotherCat.say();
|
||||
|
||||
var anotherTiger = Object.create(tiger);
|
||||
|
||||
anotherTiger.say();
|
||||
|
||||
|
||||
```
|
||||
|
||||
这段代码创建了一个“猫”对象,又根据猫做了一些修改创建了虎,之后我们完全可以用Object.create来创建另外的猫和虎对象,我们可以通过“原始猫对象”和“原始虎对象”来控制所有猫和虎的行为。
|
||||
|
||||
但是,在更早的版本中,程序员只能通过Java风格的类接口来操纵原型运行时,可以说非常别扭。
|
||||
|
||||
考虑到new和prototype属性等基础设施今天仍然有效,而且被很多代码使用,学习这些知识也有助于我们理解运行时的原型工作原理,下面我们试着回到过去,追溯一下早年的JavaScript中的原型和类。
|
||||
|
||||
## 早期版本中的类与原型
|
||||
|
||||
在早期版本的JavaScript中,“类”的定义是一个私有属性 [[class]],语言标准为内置类型诸如Number、String、Date等指定了[[class]]属性,以表示它们的类。语言使用者唯一可以访问[[class]]属性的方式是Object.prototype.toString。
|
||||
|
||||
以下代码展示了所有具有内置class属性的对象:
|
||||
|
||||
```
|
||||
var o = new Object;
|
||||
var n = new Number;
|
||||
var s = new String;
|
||||
var b = new Boolean;
|
||||
var d = new Date;
|
||||
var arg = function(){ return arguments }();
|
||||
var r = new RegExp;
|
||||
var f = new Function;
|
||||
var arr = new Array;
|
||||
var e = new Error;
|
||||
console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));
|
||||
|
||||
```
|
||||
|
||||
因此,在ES3和之前的版本,JS中类的概念是相当弱的,它仅仅是运行时的一个字符串属性。
|
||||
|
||||
在ES5开始,[[class]] 私有属性被 Symbol.toStringTag 代替,Object.prototype.toString 的意义从命名上不再跟 class 相关。我们甚至可以自定义 Object.prototype.toString 的行为,以下代码展示了使用Symbol.toStringTag来自定义 Object.prototype.toString 的行为:
|
||||
|
||||
```
|
||||
var o = { [Symbol.toStringTag]: "MyObject" }
|
||||
console.log(o + "");
|
||||
|
||||
```
|
||||
|
||||
这里创建了一个新对象,并且给它唯一的一个属性 Symbol.toStringTag,我们用字符串加法触发了Object.prototype.toString的调用,发现这个属性最终对Object.prototype.toString 的结果产生了影响。
|
||||
|
||||
但是,考虑到JavaScript语法中跟Java相似的部分,我们对类的讨论不能用“new运算是针对构造器对象,而不是类”来试图回避。
|
||||
|
||||
所以,我们仍然要把new理解成JavaScript面向对象的一部分,下面我就来讲一下new操作具体做了哪些事情。
|
||||
|
||||
new 运算接受一个构造器和一组调用参数,实际上做了几件事:
|
||||
|
||||
- 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
|
||||
- 将 this 和调用参数传给构造器,执行;
|
||||
- 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。
|
||||
|
||||
new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。
|
||||
|
||||
下面代码展示了用构造器模拟类的两种方法:
|
||||
|
||||
```
|
||||
|
||||
function c1(){
|
||||
this.p1 = 1;
|
||||
this.p2 = function(){
|
||||
console.log(this.p1);
|
||||
}
|
||||
}
|
||||
var o1 = new c1;
|
||||
o1.p2();
|
||||
|
||||
|
||||
|
||||
function c2(){
|
||||
}
|
||||
c2.prototype.p1 = 1;
|
||||
c2.prototype.p2 = function(){
|
||||
console.log(this.p1);
|
||||
}
|
||||
|
||||
var o2 = new c2;
|
||||
o2.p2();
|
||||
|
||||
```
|
||||
|
||||
第一种方法是直接在构造器中修改this,给this添加属性。
|
||||
|
||||
第二种方法是修改构造器的prototype属性指向的对象,它是从这个构造器构造出来的所有对象的原型。
|
||||
|
||||
没有Object.create、Object.setPrototypeOf 的早期版本中,new 运算是唯一一个可以指定[[prototype]]的方法(当时的mozilla提供了私有属性__proto__,但是多数环境并不支持),所以,当时已经有人试图用它来代替后来的 Object.create,我们甚至可以用它来实现一个Object.create的不完整的polyfill,见以下代码:
|
||||
|
||||
```
|
||||
Object.create = function(prototype){
|
||||
var cls = function(){}
|
||||
cls.prototype = prototype;
|
||||
return new cls;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码创建了一个空函数作为类,并把传入的原型挂在了它的prototype,最后创建了一个它的实例,根据new的行为,这将产生一个以传入的第一个参数为原型的对象。
|
||||
|
||||
这个函数无法做到与原生的Object.create一致,一个是不支持第二个参数,另一个是不支持null作为原型,所以放到今天意义已经不大了。
|
||||
|
||||
## ES6 中的类
|
||||
|
||||
好在ES6中加入了新特性class,new跟function搭配的怪异行为终于可以退休了(虽然运行时没有改变),在任何场景,我都推荐使用ES6的语法来定义类,而令function回归原本的函数语义。下面我们就来看一下ES6中的类。
|
||||
|
||||
ES6中引入了class关键字,并且在标准中删除了所有[[class]]相关的私有属性描述,类的概念正式从属性升级成语言的基础设施,从此,基于类的编程方式成为了JavaScript的官方编程范式。
|
||||
|
||||
我们先看下类的基本写法:
|
||||
|
||||
```
|
||||
class Rectangle {
|
||||
constructor(height, width) {
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
}
|
||||
// Getter
|
||||
get area() {
|
||||
return this.calcArea();
|
||||
}
|
||||
// Method
|
||||
calcArea() {
|
||||
return this.height * this.width;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在现有的类语法中,getter/setter和method是兼容性最好的。
|
||||
|
||||
我们通过get/set关键字来创建getter,通过括号和大括号来创建方法,数据型成员最好写在构造器里面。
|
||||
|
||||
类的写法实际上也是由原型运行时来承载的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象之上。
|
||||
|
||||
此外,最重要的是,类提供了继承能力。我们来看一下下面的代码。
|
||||
|
||||
```
|
||||
class Animal {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
speak() {
|
||||
console.log(this.name + ' makes a noise.');
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
constructor(name) {
|
||||
super(name); // call the super class constructor and pass in the name parameter
|
||||
}
|
||||
|
||||
speak() {
|
||||
console.log(this.name + ' barks.');
|
||||
}
|
||||
}
|
||||
|
||||
let d = new Dog('Mitzie');
|
||||
d.speak(); // Mitzie barks.
|
||||
|
||||
```
|
||||
|
||||
以上代码创造了Animal类,并且通过extends关键字让Dog继承了它,展示了最终调用子类的speak方法获取了父类的name。
|
||||
|
||||
比起早期的原型模拟方式,使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数,这是一种更少坑的设计。
|
||||
|
||||
所以当我们使用类的思想来设计代码时,应该尽量使用class来声明类,而不是用旧语法,拿函数来模拟对象。
|
||||
|
||||
一些激进的观点认为,class关键字和箭头运算符可以完全替代旧的function关键字,它更明确地区分了定义函数和定义类两种意图,我认为这是有一定道理的。
|
||||
|
||||
## 总结
|
||||
|
||||
在新的ES版本中,我们不再需要模拟类了:我们有了光明正大的新语法。而原型体系同时作为一种编程范式和运行时机制存在。
|
||||
|
||||
我们可以自由选择原型或者类作为代码的抽象风格,但是无论我们选择哪种,理解运行时的原型系统都是很有必要的一件事。
|
||||
|
||||
在你的工作中,是使用class还是仍然在用function来定义“类”?为什么这么做?如何把使用function定义类的代码改造到class的新语法?
|
||||
|
||||
欢迎给我留言,我们一起讨论。
|
||||
|
||||
|
||||
204
极客时间专栏/重学前端/模块一:JavaScript/JavaScript对象:面向对象还是基于对象?.md
Normal file
204
极客时间专栏/重学前端/模块一:JavaScript/JavaScript对象:面向对象还是基于对象?.md
Normal file
@@ -0,0 +1,204 @@
|
||||
<audio id="audio" title="JavaScript对象:面向对象还是基于对象?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/fe/4cdb714b72874758f2e7a7ce975eb1fe.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
与其它的语言相比,JavaScript中的“对象”总是显得不那么合群。
|
||||
|
||||
一些新人在学习JavaScript面向对象时,往往也会有疑惑:
|
||||
|
||||
- 为什么JavaScript(直到ES6)有对象的概念,但是却没有像其他的语言那样,有类的概念呢;
|
||||
- 为什么在JavaScript对象里可以自由添加属性,而其他的语言却不能呢?
|
||||
|
||||
甚至,在一些争论中,有人强调:JavaScript并非“面向对象的语言”,而是“基于对象的语言”。这个说法一度流传甚广,而事实上,我至今遇到的持有这一说法的人中,无一能够回答“如何定义面向对象和基于对象”这个问题。
|
||||
|
||||
实际上,基于对象和面向对象两个形容词都出现在了JavaScript标准的各个版本当中。
|
||||
|
||||
我们可以先看看JavaScript标准对基于对象的定义,这个定义的具体内容是:“语言和宿主的基础设施由对象来提供,并且JavaScript程序即是一系列互相通讯的对象集合”。
|
||||
|
||||
这里的意思根本不是表达弱化的面向对象的意思,反而是表达对象对于语言的重要性。
|
||||
|
||||
那么,在本篇文章中,我会尝试让你去理解面向对象和JavaScript中的面向对象究竟是什么。
|
||||
|
||||
## 什么是面向对象?
|
||||
|
||||
我们先来说说什么是对象,因为翻译的原因,中文语境下我们很难理解“对象”的真正含义。事实上,Object(对象)在英文中,是一切事物的总称,这和面向对象编程的抽象思维有互通之处。
|
||||
|
||||
中文的“对象”却没有这样的普适性,我们在学习编程的过程中,更多是把它当作一个专业名词来理解。
|
||||
|
||||
但不论如何,我们应该认识到,对象并不是计算机领域凭空造出来的概念,它是顺着人类思维模式产生的一种抽象(于是面向对象编程也被认为是:更接近人类思维模式的一种编程范式)。
|
||||
|
||||
那么,我们先来看看在人类思维模式下,对象究竟是什么。
|
||||
|
||||
>
|
||||
对象这一概念在人类的幼儿期形成,这远远早于我们编程逻辑中常用的值、过程等概念。
|
||||
在幼年期,我们总是先认识到某一个苹果能吃(这里的某一个苹果就是一个对象),继而认识到所有的苹果都可以吃(这里的所有苹果,就是一个类),再到后来我们才能意识到三个苹果和三个梨之间的联系,进而产生数字“3”(值)的概念。
|
||||
|
||||
|
||||
在《面向对象分析与设计》这本书中,Grady Booch替我们做了总结,他认为,从人类的认知角度来说,对象应该是下列事物之一:
|
||||
|
||||
1. 一个可以触摸或者可以看见的东西;
|
||||
1. 人的智力可以理解的东西;
|
||||
1. 可以指导思考或行动(进行想象或施加动作)的东西。
|
||||
|
||||
有了对象的自然定义后,我们就可以描述编程语言中的对象了。在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java等流行的编程语言。
|
||||
|
||||
而 JavaScript 早年却选择了一个更为冷门的方式:原型(关于原型,我在下一篇文章会重点介绍,这里你留个印象就可以了)。这是我在前面说它不合群的原因之一。
|
||||
|
||||
然而很不幸,因为一些公司政治原因,JavaScript推出之时受管理层之命被要求模仿Java,所以,JavaScript创始人Brendan Eich在“原型运行时”的基础上引入了new、this等语言特性,使之“看起来更像Java”。
|
||||
|
||||
在 ES6 出现之前,大量的 JavaScript 程序员试图在原型体系的基础上,把JavaScript变得更像是基于类的编程,进而产生了很多所谓的“框架”,比如PrototypeJS、Dojo。
|
||||
|
||||
事实上,它们成为了某种JavaScript的古怪方言,甚至产生了一系列互不相容的社群,显然这样做的收益是远远小于损失的。
|
||||
|
||||
如果我们从运行时角度来谈论对象,就是在讨论JavaScript实际运行中的模型,这是由于任何代码执行都必定绕不开运行时的对象模型。
|
||||
|
||||
不过,幸运的是,从运行时的角度看,可以不必受到这些“基于类的设施”的困扰,这是因为任何语言运行时类的概念都是被弱化的。
|
||||
|
||||
首先我们来了解一下JavaScript是如何设计对象模型的。
|
||||
|
||||
## JavaScript 对象的特征
|
||||
|
||||
在我看来,不论我们使用什么样的编程语言,我们都先应该去理解对象的本质特征(参考Grandy Booch《面向对象分析与设计》)。总结来看,对象有如下几个特点。
|
||||
|
||||
- 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
|
||||
- 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
|
||||
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
|
||||
|
||||
我们先来看第一个特征,对象具有唯一标识性。一般而言,各种语言的对象唯一标识性都是用内存地址来体现的, 对象具有唯一标识的内存地址,所以具有唯一的标识。
|
||||
|
||||
所以,JavaScript程序员都知道,任何不同的JavaScript对象其实是互不相等的,我们可以看下面的代码,o1和o2初看是两个一模一样的对象,但是打印出来的结果却是false。
|
||||
|
||||
```
|
||||
var o1 = { a: 1 };
|
||||
var o2 = { a: 1 };
|
||||
console.log(o1 == o2); // false
|
||||
|
||||
```
|
||||
|
||||
关于对象的第二个和第三个特征“状态和行为”,不同语言会使用不同的术语来抽象描述它们,比如C++中称它们为“成员变量”和“成员函数”,Java中则称它们为“属性”和“方法”。
|
||||
|
||||
在 JavaScript中,将状态和行为统一抽象为“属性”,考虑到 JavaScript 中将函数设计成一种特殊对象(关于这点,我会在后面的文章中详细讲解,此处先不用细究),所以 JavaScript中的行为和状态都能用属性来抽象。
|
||||
|
||||
下面这段代码其实就展示了普通属性和函数作为属性的一个例子,其中o是对象,d是一个属性,而函数f也是一个属性,尽管写法不太相同,但是对JavaScript来说,d和f就是两个普通属性。
|
||||
|
||||
```
|
||||
var o = {
|
||||
d: 1,
|
||||
f() {
|
||||
console.log(this.d);
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
所以,总结一句话来看,在JavaScript中,对象的状态和行为其实都被抽象为了属性。如果你用过Java,一定不要觉得奇怪,尽管设计思路有一定差别,但是二者都很好地表现了对象的基本特征:标识性、状态和行为。
|
||||
|
||||
**在实现了对象基本特征的基础上, 我认为,JavaScript中对象独有的特色是:对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力。**
|
||||
|
||||
我来举个例子,比如,JavaScript 允许运行时向对象添加属性,这就跟绝大多数基于类的、静态的对象设计完全不同。如果你用过Java或者其它别的语言,肯定会产生跟我一样的感受。
|
||||
|
||||
下面这段代码就展示了运行时如何向一个对象添加属性,一开始我定义了一个对象o,定义完成之后,再添加它的属性b,这样操作是完全没问题的。
|
||||
|
||||
```
|
||||
var o = { a: 1 };
|
||||
o.b = 2;
|
||||
console.log(o.a, o.b); //1 2
|
||||
|
||||
```
|
||||
|
||||
为了提高抽象能力,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。
|
||||
|
||||
## JavaScript对象的两类属性
|
||||
|
||||
对JavaScript来说,属性并非只是简单的名称和值,JavaScript用一组特征(attribute)来描述属性(property)。
|
||||
|
||||
先来说第一类属性,数据属性。它比较接近于其它语言的属性概念。数据属性具有四个特征。
|
||||
|
||||
- value:就是属性的值。
|
||||
- writable:决定属性能否被赋值。
|
||||
- enumerable:决定for in能否枚举该属性。
|
||||
- configurable:决定该属性能否被删除或者改变特征值。
|
||||
|
||||
在大多数情况下,我们只关心数据属性的值即可。
|
||||
|
||||
第二类属性是访问器(getter/setter)属性,它也有四个特征。
|
||||
|
||||
- getter:函数或undefined,在取属性值时被调用。
|
||||
- setter:函数或undefined,在设置属性值时被调用。
|
||||
- enumerable:决定for in能否枚举该属性。
|
||||
- configurable:决定该属性能否被删除或者改变特征值。
|
||||
|
||||
访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。
|
||||
|
||||
我们通常用于定义属性的代码会产生数据属性,其中的writable、enumerable、configurable都默认为true。我们可以使用内置函数getOwnPropertyDescriptor来查看,如以下代码所示:
|
||||
|
||||
```
|
||||
var o = { a: 1 };
|
||||
o.b = 2;
|
||||
//a和b皆为数据属性
|
||||
Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
|
||||
Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
|
||||
|
||||
```
|
||||
|
||||
我们在这里使用了两种语法来定义属性,定义完属性后,我们用JavaScript的API来查看这个属性,我们可以发现,这样定义出来的属性都是数据属性,writeable、enumerable、configurable都是默认值为true。
|
||||
|
||||
如果我们要想改变属性的特征,或者定义访问器属性,我们可以使用 Object.defineProperty,示例如下:
|
||||
|
||||
```
|
||||
var o = { a: 1 };
|
||||
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
|
||||
//a和b都是数据属性,但特征值变化了
|
||||
Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
|
||||
Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
|
||||
o.b = 3;
|
||||
console.log(o.b); // 2
|
||||
|
||||
```
|
||||
|
||||
这里我们使用了Object.defineProperty来定义属性,这样定义属性可以改变属性的writable和enumerable。
|
||||
|
||||
我们同样用Object.getOwnPropertyDescriptor来查看,发现确实改变了writable和enumerable特征。因为writable特征为false,所以我们重新对b赋值,b的值不会发生变化。
|
||||
|
||||
在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性,代码如下所示:
|
||||
|
||||
```
|
||||
var o = { get a() { return 1 } };
|
||||
|
||||
console.log(o.a); // 1
|
||||
|
||||
```
|
||||
|
||||
访问器属性跟数据属性不同,每次访问属性都会执行getter或者setter函数。这里我们的getter函数返回了1,所以o.a每次都得到1。
|
||||
|
||||
这样,我们就理解了,实际上JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者Symbol为key,以数据属性特征值或者访问器属性特征值为value。
|
||||
|
||||
对象是一个属性的索引结构(索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用key来查找value的字典)。我们以上面的对象o为例,你可以想象一下“a”是key。
|
||||
|
||||
`{writable:true,value:1,configurable:true,enumerable:true}`是value。我们在前面的类型课程中,已经介绍了Symbol类型,能够以Symbol为属性名,这是JavaScript对象的一个特色。
|
||||
|
||||
讲到了这里,如果你理解了对象的特征,也就不难理解我开篇提出来的问题。
|
||||
|
||||
你甚至可以理解为什么会有“JavaScript不是面向对象”这样的说法了。这是由于JavaScript的对象设计跟目前主流基于类的面向对象差异非常大。
|
||||
|
||||
可事实上,这样的对象系统设计虽然特别,但是JavaScript提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式(下一节课我们会给你介绍JavaScript中两种面向对象编程的范式:基于类和基于原型),所以它也是正统的面向对象语言。
|
||||
|
||||
JavaScript语言标准也已经明确说明,JavaScript是一门面向对象的语言,我想标准中能这样说,正是因为JavaScript的高度动态性的对象系统。
|
||||
|
||||
所以,我们应该在理解其设计思想的基础上充分挖掘它的能力,而不是机械地模仿其它语言。
|
||||
|
||||
## 结语
|
||||
|
||||
要想理解JavaScript对象,必须清空我们脑子里“基于类的面向对象”相关的知识,回到人类对对象的朴素认知和面向对象的语言无关基础理论,我们就能够理解JavaScript面向对象设计的思路。
|
||||
|
||||
在这篇文章中,我从对象的基本理论出发,和你理清了关于对象的一些基本概念,分析了JavaScript对象的设计思路。接下来又从运行时的角度,介绍了JavaScript对象的具体设计:具有高度动态性的属性集合。
|
||||
|
||||
很多人在思考JavaScript对象时,会带着已有的“对象”观来看问题,最后的结果当然就是“剪不断理还乱”了。
|
||||
|
||||
在后面的文章中,我会继续带你探索JavaScript对象的一些机制,看JavaScript如何基于这样的动态对象模型设计自己的原型系统,以及你熟悉的函数、类等基础设施。
|
||||
|
||||
你还知道哪些面向对象语言,它们的面向对象系统是怎样的?请留言告诉我吧!
|
||||
|
||||
# 猜你喜欢
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/1a/08/1a49758821bdbdf6f0a8a1dc5bf39f08.jpg" alt="unpreview">](https://time.geekbang.org/course/intro/163?utm_term=zeusMTA7L&utm_source=app&utm_medium=chongxueqianduan&utm_campaign=163-presell)
|
||||
@@ -0,0 +1,205 @@
|
||||
<audio id="audio" title="JavaScript执行(一):Promise里的代码为什么比setTimeout先执行?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/65/68/651494b39e6d5304727e745cd50bfa68.mp3"></audio>
|
||||
|
||||
你好,我是winter。这一部分我们来讲一讲JavaScript的执行。
|
||||
|
||||
首先我们考虑一下,如果我们是浏览器或者Node的开发者,我们该如何使用JavaScript引擎。
|
||||
|
||||
当拿到一段JavaScript代码时,浏览器或者Node环境首先要做的就是;传递给JavaScript引擎,并且要求它去执行。
|
||||
|
||||
然而,执行JavaScript并非一锤子买卖,宿主环境当遇到一些事件时,会继续把一段代码传递给JavaScript引擎去执行,此外,我们可能还会提供API给JavaScript引擎,比如setTimeout这样的API,它会允许JavaScript在特定的时机执行。
|
||||
|
||||
所以,我们首先应该形成一个感性的认知:一个JavaScript引擎会常驻于内存中,它等待着我们(宿主)把JavaScript代码或者函数传递给它执行。
|
||||
|
||||
在ES3和更早的版本中,JavaScript本身还没有异步执行代码的能力,这也就意味着,宿主环境传递给JavaScript引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。
|
||||
|
||||
但是,在ES5之后,JavaScript引入了Promise,这样,不需要浏览器的安排,JavaScript引擎本身也可以发起任务了。
|
||||
|
||||
由于我们这里主要讲JavaScript语言,那么采纳JSC引擎的术语,我们把宿主发起的任务称为宏观任务,把JavaScript引擎发起的任务称为微观任务。
|
||||
|
||||
## 宏观和微观任务
|
||||
|
||||
JavaScript引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环,所以在Node术语中,也会把这个部分称为事件循环。
|
||||
|
||||
不过,术语本身并非我们需要重点讨论的内容,我们在这里把重点放在事件循环的原理上。在底层的C/C++代码中,这个事件循环是一个跑在独立线程中的循环,我们用伪代码来表示,大概是这样的:
|
||||
|
||||
```
|
||||
while(TRUE) {
|
||||
r = wait();
|
||||
execute(r);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,整个循环做的事情基本上就是反复“等待-执行”。当然,实际的代码中并没有这么简单,还有要判断循环是否结束、宏观任务队列等逻辑,这里为了方便你理解,我就把这些都省略掉了。
|
||||
|
||||
这里每次的执行过程,其实都是一个宏观任务。我们可以大概理解:宏观任务的队列就相当于事件循环。
|
||||
|
||||
在宏观任务中,JavaScript的Promise还会产生异步代码,JavaScript必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/65/16f70a9a51a65d5302166b0d78414d65.jpg" alt="">
|
||||
|
||||
有了宏观任务和微观任务机制,我们就可以实现JavaScript引擎级和宿主级的任务了,例如:Promise永远在队列尾部添加微观任务。setTimeout等宿主API,则会添加宏观任务。
|
||||
|
||||
接下来,我们来详细介绍一下Promise。
|
||||
|
||||
## Promise
|
||||
|
||||
Promise是JavaScript语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过Promise的then方法的回调)。
|
||||
|
||||
Promise的基本用法示例如下:
|
||||
|
||||
```
|
||||
function sleep(duration) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve,duration);
|
||||
})
|
||||
}
|
||||
sleep(1000).then( ()=> console.log("finished"));
|
||||
|
||||
```
|
||||
|
||||
这段代码定义了一个函数sleep,它的作用是等候传入参数指定的时长。
|
||||
|
||||
Promise的then回调是一个异步的执行过程,下面我们就来研究一下Promise函数中的执行顺序,我们来看一段代码示例:
|
||||
|
||||
```
|
||||
var r = new Promise(function(resolve, reject){
|
||||
console.log("a");
|
||||
resolve()
|
||||
});
|
||||
r.then(() => console.log("c"));
|
||||
console.log("b")
|
||||
|
||||
```
|
||||
|
||||
我们执行这段代码后,注意输出的顺序是 a b c。在进入console.log(“b”) 之前,毫无疑问 r 已经得到了resolve,但是Promise的resolve始终是异步操作,所以c无法出现在b之前。
|
||||
|
||||
接下来我们试试跟setTimeout混用的Promise。
|
||||
|
||||
在这段代码中,我设置了两段互不相干的异步操作:通过setTimeout执行console.log(“d”),通过Promise执行console.log(“c”)。
|
||||
|
||||
```
|
||||
var r = new Promise(function(resolve, reject){
|
||||
console.log("a");
|
||||
resolve()
|
||||
});
|
||||
setTimeout(()=>console.log("d"), 0)
|
||||
r.then(() => console.log("c"));
|
||||
console.log("b")
|
||||
|
||||
```
|
||||
|
||||
我们发现,不论代码顺序如何,d必定发生在c之后,因为Promise产生的是JavaScript引擎内部的微任务,而setTimeout是浏览器API,它产生宏任务。
|
||||
|
||||
为了理解微任务始终先于宏任务,我们设计一个实验:执行一个耗时1秒的Promise。
|
||||
|
||||
```
|
||||
setTimeout(()=>console.log("d"), 0)
|
||||
var r = new Promise(function(resolve, reject){
|
||||
resolve()
|
||||
});
|
||||
r.then(() => {
|
||||
var begin = Date.now();
|
||||
while(Date.now() - begin < 1000);
|
||||
console.log("c1")
|
||||
new Promise(function(resolve, reject){
|
||||
resolve()
|
||||
}).then(() => console.log("c2"))
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
这里我们强制了1秒的执行耗时,这样,我们可以确保任务c2是在d之后被添加到任务队列。
|
||||
|
||||
我们可以看到,即使耗时一秒的c1执行完毕,再enque的c2,仍然先于d执行了,这很好地解释了微任务优先的原理。
|
||||
|
||||
通过一系列的实验,我们可以总结一下如何分析异步执行的顺序:
|
||||
|
||||
- 首先我们分析有多少个宏任务;
|
||||
- 在每个宏任务中,分析有多少个微任务;
|
||||
- 根据调用次序,确定宏任务中的微任务执行次序;
|
||||
- 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
|
||||
- 确定整个顺序。
|
||||
|
||||
我们再来看一个稍微复杂的例子:
|
||||
|
||||
```
|
||||
function sleep(duration) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
console.log("b");
|
||||
setTimeout(resolve,duration);
|
||||
})
|
||||
}
|
||||
console.log("a");
|
||||
sleep(5000).then(()=>console.log("c"));
|
||||
|
||||
```
|
||||
|
||||
这是一段非常常用的封装方法,利用Promise把setTimeout封装成可以用于异步的函数。
|
||||
|
||||
我们首先来看,setTimeout把整个代码分割成了2个宏观任务,这里不论是5秒还是0秒,都是一样的。
|
||||
|
||||
第一个宏观任务中,包含了先后同步执行的 console.log(“a”); 和 console.log(“b”);。
|
||||
|
||||
setTimeout后,第二个宏观任务执行调用了resolve,然后then中的代码异步得到执行,所以调用了console.log(“c”),最终输出的顺序才是: a b c。
|
||||
|
||||
Promise是JavaScript中的一个定义,但是实际编写代码时,我们可以发现,它似乎并不比回调的方式书写更简单,但是从ES6开始,我们有了async/await,这个语法改进跟Promise配合,能够有效地改善代码结构。
|
||||
|
||||
## 新特性:async/await
|
||||
|
||||
async/await是ES2016新加入的特性,它提供了用for、if等代码结构来编写异步的方式。它的运行时基础是Promise,面对这种比较新的特性,我们先来看一下基本用法。
|
||||
|
||||
async函数必定返回Promise,我们把所有返回Promise的函数都可以认为是异步函数。
|
||||
|
||||
async函数是一种特殊语法,特征是在function关键字之前加上async关键字,这样,就定义了一个async函数,我们可以在其中使用await来等待一个Promise。
|
||||
|
||||
```
|
||||
function sleep(duration) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve,duration);
|
||||
})
|
||||
}
|
||||
async function foo(){
|
||||
console.log("a")
|
||||
await sleep(2000)
|
||||
console.log("b")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码利用了我们之前定义的sleep函数。在异步函数foo中,我们调用sleep。
|
||||
|
||||
async函数强大之处在于,它是可以嵌套的。我们在定义了一批原子操作的情况下,可以利用async函数组合出新的async函数。
|
||||
|
||||
```
|
||||
function sleep(duration) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve,duration);
|
||||
})
|
||||
}
|
||||
async function foo(name){
|
||||
await sleep(2000)
|
||||
console.log(name)
|
||||
}
|
||||
async function foo2(){
|
||||
await foo("a");
|
||||
await foo("b");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里foo2用await调用了两次异步函数foo,可以看到,如果我们把sleep这样的异步操作放入某一个框架或者库中,使用者几乎不需要了解Promise的概念即可进行异步编程了。
|
||||
|
||||
此外,generator/iterator也常常被跟异步一起来讲,我们必须说明 generator/iterator 并非异步代码,只是在缺少async/await的时候,一些框架(最著名的要数co)使用这样的特性来模拟async/await。
|
||||
|
||||
但是generator并非被设计成实现异步,所以有了async/await之后,generator/iterator来模拟异步的方法应该被废弃。
|
||||
|
||||
## 结语
|
||||
|
||||
在今天的文章里,我们学习了JavaScript执行部分的知识,首先我们学习了JavaScript的宏观任务和微观任务相关的知识。我们把宿主发起的任务称为宏观任务,把JavaScript引擎发起的任务称为微观任务。许多的微观任务的队列组成了宏观任务。
|
||||
|
||||
除此之外,我们还展开介绍了用Promise来添加微观任务的方式,并且介绍了async/await这个语法的改进。
|
||||
|
||||
最后,留给你一个小练习:我们现在要实现一个红绿灯,把一个圆形div按照绿色3秒,黄色1秒,红色2秒循环改变背景色,你会怎样编写这个代码呢?欢迎你留言讨论。
|
||||
|
||||
|
||||
315
极客时间专栏/重学前端/模块一:JavaScript/JavaScript执行(三):你知道现在有多少种函数吗?.md
Normal file
315
极客时间专栏/重学前端/模块一:JavaScript/JavaScript执行(三):你知道现在有多少种函数吗?.md
Normal file
@@ -0,0 +1,315 @@
|
||||
<audio id="audio" title="JavaScript执行(三):你知道现在有多少种函数吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/18/0093381651a5e580a13209cc6d07c918.mp3"></audio>
|
||||
|
||||
在前一篇文章中,我们大致了解了执行上下文是什么,也知道了任何语句的执行都会依赖特定的上下文。
|
||||
|
||||
一旦上下文被切换,整个语句的效果可能都会发生改变。那么,切换上下文的时机就显得非常重要了。
|
||||
|
||||
在JavaScript,切换上下文最主要的场景是函数调用。在这一课,我们就来讲讲函数调用切换上下文的事情。我们在讲函数调用之前,首先来认识一下函数家族。
|
||||
|
||||
## 函数
|
||||
|
||||
在ES2018中,函数已经是一个很复杂的体系了,我在这里整理了一下。
|
||||
|
||||
**第一种,普通函数:用function关键字定义的函数。**
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
function foo(){
|
||||
// code
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第二种,箭头函数:用 => 运算符定义的函数。**
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
const foo = () => {
|
||||
// code
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第三种,方法:在class中定义的函数。**
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
class C {
|
||||
foo(){
|
||||
//code
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第四种,生成器函数:用function * 定义的函数。**
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
function* foo(){
|
||||
// code
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第五种,类:用class定义的类,实际上也是函数。**
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
class Foo {
|
||||
constructor(){
|
||||
//code
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第六/七/八种,异步函数:普通函数、箭头函数和生成器函数加上async关键字。**
|
||||
|
||||
示例:
|
||||
|
||||
```
|
||||
async function foo(){
|
||||
// code
|
||||
}
|
||||
const foo = async () => {
|
||||
// code
|
||||
}
|
||||
async function foo*(){
|
||||
// code
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
ES6以来,大量加入的新语法极大地方便了我们编程的同时,也增加了很多我们理解的心智负担。要想认识这些函数的执行上下文切换,我们必须要对它们行为上的区别有所了解。
|
||||
|
||||
对普通变量而言,这些函数并没有本质区别,都是遵循了“继承定义时环境”的规则,它们的一个行为差异在于this关键字。
|
||||
|
||||
那么,this关键字是什么呢,我们一起来看一看。
|
||||
|
||||
## this关键字的行为
|
||||
|
||||
this是JavaScript中的一个关键字,它的使用方法类似于一个变量(但是this跟变量的行为有很多不同,上一节课我们讲了一些普通变量的行为和机制,也就是var声明和赋值、let的内容)。
|
||||
|
||||
**this是执行上下文中很重要的一个组成部分。同一个函数调用方式不同,得到的this值也不同**,我们看一个例子:
|
||||
|
||||
```
|
||||
function showThis(){
|
||||
console.log(this);
|
||||
}
|
||||
|
||||
var o = {
|
||||
showThis: showThis
|
||||
}
|
||||
|
||||
showThis(); // global
|
||||
o.showThis(); // o
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,我们定义了函数showThis,我们把它赋值给一个对象o的属性,然后尝试分别使用两个引用来调用同一个函数,结果得到了不同的this值。
|
||||
|
||||
普通函数的this值由“调用它所使用的引用”决定,其中奥秘就在于:我们获取函数的表达式,它实际上返回的并非函数本身,而是一个Reference类型(记得我们在类型一章讲过七种标准类型吗,正是其中之一)。
|
||||
|
||||
Reference类型由两部分组成:一个对象和一个属性值。不难理解 o.showThis 产生的Reference类型,即由对象o和属性“showThis”构成。
|
||||
|
||||
当做一些算术运算(或者其他运算时),Reference类型会被解引用,即获取真正的值(被引用的内容)来参与运算,而类似函数调用、delete等操作,都需要用到Reference类型中的对象。
|
||||
|
||||
在这个例子中,Reference类型中的对象被当作this值,传入了执行函数时的上下文当中。
|
||||
|
||||
至此,我们对this的解释已经非常清晰了:**调用函数时使用的引用,决定了函数执行时刻的this值。**
|
||||
|
||||
实际上从运行时的角度来看,this跟面向对象毫无关联,它是与函数调用时使用的表达式相关。
|
||||
|
||||
这个设计来自JavaScript早年,通过这样的方式,巧妙地模仿了Java的语法,但是仍然保持了纯粹的“无类”运行时设施。
|
||||
|
||||
如果,我们把这个例子稍作修改,换成箭头函数,结果就不一样了:
|
||||
|
||||
```
|
||||
const showThis = () => {
|
||||
console.log(this);
|
||||
}
|
||||
|
||||
var o = {
|
||||
showThis: showThis
|
||||
}
|
||||
|
||||
showThis(); // global
|
||||
o.showThis(); // global
|
||||
|
||||
```
|
||||
|
||||
**我们看到,改为箭头函数后,不论用什么引用来调用它,都不影响它的this值。**
|
||||
|
||||
接下来我们看看“方法”,它的行为又不一样了:
|
||||
|
||||
```
|
||||
class C {
|
||||
showThis() {
|
||||
console.log(this);
|
||||
}
|
||||
}
|
||||
var o = new C();
|
||||
var showThis = o.showThis;
|
||||
|
||||
showThis(); // undefined
|
||||
o.showThis(); // o
|
||||
|
||||
```
|
||||
|
||||
这里我们创建了一个类C,并且实例化出对象o,再把o的方法赋值给了变量showThis。
|
||||
|
||||
这时候,我们使用showThis这个引用去调用方法时,得到了undefined。
|
||||
|
||||
所以,在方法中,我们看到this的行为也不太一样,它得到了undefined的结果。
|
||||
|
||||
按照我们上面的方法,不难验证出:生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的,异步箭头函数与箭头函数行为是一致的。
|
||||
|
||||
## this关键字的机制
|
||||
|
||||
说完了this行为,我们再来简单谈谈在JavaScript内部,实现this这些行为的机制,让你对这部分知识有一个大概的认知。
|
||||
|
||||
函数能够引用定义时的变量,如上文分析,函数也能记住定义时的this,因此,函数内部必定有一个机制来保存这些信息。
|
||||
|
||||
在JavaScript标准中,为函数规定了用来保存定义时上下文的私有属性[[Environment]]。
|
||||
|
||||
当一个函数执行时,会创建一条新的执行环境记录,记录的外层词法环境(outer lexical environment)会被设置成函数的[[Environment]]。
|
||||
|
||||
这个动作就是**切换上下文**了,我们假设有这样的代码:
|
||||
|
||||
```
|
||||
var a = 1;
|
||||
foo();
|
||||
|
||||
在别处定义了foo:
|
||||
|
||||
var b = 2;
|
||||
function foo(){
|
||||
console.log(b); // 2
|
||||
console.log(a); // error
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里的foo能够访问b(定义时词法环境),却不能访问a(执行时的词法环境),这就是执行上下文的切换机制了。
|
||||
|
||||
JavaScript用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表。如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e8/31/e8d8e96c983a832eb646d6c17ff3df31.jpg" alt="">
|
||||
|
||||
当函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。
|
||||
|
||||
而this则是一个更为复杂的机制,JavaScript标准定义了 [[thisMode]] 私有属性。
|
||||
|
||||
[[thisMode]] 私有属性有三个取值。
|
||||
|
||||
- lexical:表示从上下文中找this,这对应了箭头函数。
|
||||
- global:表示当this为undefined时,取全局对象,对应了普通函数。
|
||||
- strict:当严格模式时使用,this严格按照调用时传入的值,可能为null或者undefined。
|
||||
|
||||
非常有意思的是,方法的行为跟普通函数有差异,恰恰是因为class设计成了默认按strict模式执行。
|
||||
|
||||
我们可以用strict达成与上一节中方法的例子一样的效果:
|
||||
|
||||
```
|
||||
"use strict"
|
||||
function showThis(){
|
||||
console.log(this);
|
||||
}
|
||||
|
||||
var o = {
|
||||
showThis: showThis
|
||||
}
|
||||
|
||||
showThis(); // undefined
|
||||
o.showThis(); // o
|
||||
|
||||
```
|
||||
|
||||
函数创建新的执行上下文中的词法环境记录时,会根据[[thisMode]]来标记新纪录的[[ThisBindingStatus]]私有属性。
|
||||
|
||||
代码执行遇到this时,会逐层检查当前词法环境记录中的[[ThisBindingStatus]],当找到有this的环境记录时获取this的值。
|
||||
|
||||
这样的规则的实际效果是,嵌套的箭头函数中的代码都指向外层this,例如:
|
||||
|
||||
```
|
||||
var o = {}
|
||||
o.foo = function foo(){
|
||||
console.log(this);
|
||||
return () => {
|
||||
console.log(this);
|
||||
return () => console.log(this);
|
||||
}
|
||||
}
|
||||
|
||||
o.foo()()(); // o, o, o
|
||||
|
||||
```
|
||||
|
||||
这个例子中,我们定义了三层嵌套的函数,最外层为普通函数,两层都是箭头函数。
|
||||
|
||||
这里调用三个函数,获得的this值是一致的,都是对象o。
|
||||
|
||||
JavaScript还提供了一系列函数的内置方法来操纵this值,下面我们来了解一下。
|
||||
|
||||
## 操作this的内置函数
|
||||
|
||||
Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的this值,示例如下:
|
||||
|
||||
```
|
||||
function foo(a, b, c){
|
||||
console.log(this);
|
||||
console.log(a, b, c);
|
||||
}
|
||||
foo.call({}, 1, 2, 3);
|
||||
foo.apply({}, [1, 2, 3]);
|
||||
|
||||
```
|
||||
|
||||
这里call和apply作用是一样的,只是传参方式有区别。
|
||||
|
||||
此外,还有 Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的this值固定了参数:
|
||||
|
||||
```
|
||||
function foo(a, b, c){
|
||||
console.log(this);
|
||||
console.log(a, b, c);
|
||||
}
|
||||
foo.bind({}, 1, 2, 3)();
|
||||
|
||||
```
|
||||
|
||||
有趣的是,call、bind和apply用于不接受this的函数类型如箭头、class都不会报错。
|
||||
|
||||
这时候,它们无法实现改变this的能力,但是可以实现传参。
|
||||
|
||||
## 结语
|
||||
|
||||
在这一节课程中,我们认识了ES2018中规定的各种函数,我一共简单介绍了8种函数。
|
||||
|
||||
我们围绕this这个中心,介绍了函数的执行上下文切换机制。同时我们还讲解了this中的一些相关知识。包括了操作this的内置函数。
|
||||
|
||||
最后,留给你一个问题,你在日常开发中用过哪些函数类型呢?欢迎给我留言,我们一起讨论。
|
||||
|
||||
## 补充阅读:new与this
|
||||
|
||||
我们在之前的对象部分已经讲过new的执行过程,我们再来看一下:
|
||||
|
||||
- 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
|
||||
- 将 this 和调用参数传给构造器,执行;
|
||||
- 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。
|
||||
|
||||
显然,通过new调用函数,跟直接调用的this取值有明显区别。那么我们今天讲的这些函数跟new搭配又会产生什么效果呢?
|
||||
|
||||
这里我整理了一张表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/da/6a9f0525b713a903c6c94f52afaea3da.png" alt="">
|
||||
|
||||
我们可以看到,仅普通函数和类能够跟new搭配使用,这倒是给我们省去了不少麻烦。
|
||||
|
||||
|
||||
251
极客时间专栏/重学前端/模块一:JavaScript/JavaScript执行(二):闭包和执行上下文到底是怎么回事?.md
Normal file
251
极客时间专栏/重学前端/模块一:JavaScript/JavaScript执行(二):闭包和执行上下文到底是怎么回事?.md
Normal file
@@ -0,0 +1,251 @@
|
||||
<audio id="audio" title="JavaScript执行(二):闭包和执行上下文到底是怎么回事?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/9b/0071a87b7ecb2e9a93c7d12a93946d9b.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
在上一课,我们了解了JavaScript执行中最粗粒度的任务:传给引擎执行的代码段。并且,我们还根据“由JavaScript引擎发起”还是“由宿主发起”,分成了宏观任务和微观任务,接下来我们继续去看一看更细的执行粒度。
|
||||
|
||||
一段JavaScript代码可能会包含函数调用的相关内容,从今天开始,我们就用两节课的时间来了解一下函数的执行。
|
||||
|
||||
我们今天要讲的知识在网上有不同的名字,比较常见的可能有:
|
||||
|
||||
- 闭包;
|
||||
- 作用域链;
|
||||
- 执行上下文;
|
||||
- this值。
|
||||
|
||||
实际上,尽管它们是表示不同的意思的术语,所指向的几乎是同一部分知识,那就是函数执行过程相关的知识。我们可以简单看一下图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/68/52/68f50c00d475a7d6d8c7eef6a91b2152.png" alt="">
|
||||
|
||||
看着也许会有点晕,别着急,我会和你共同理一下它们之间的关系。
|
||||
|
||||
当然,除了让你理解函数执行过程的知识,理清这些概念也非常重要。所以我们先来讲讲这个有点复杂的概念:闭包。
|
||||
|
||||
## 闭包
|
||||
|
||||
闭包翻译自英文单词closure,这是个不太好翻译的词,在计算机领域,它就有三个完全不相同的意义:编译原理中,它是处理语法产生式的一个步骤;计算几何中,它表示包裹平面点集的凸多边形(翻译作凸包);而在编程语言领域,它表示一种函数。
|
||||
|
||||
闭包这个概念第一次出现在1964年的《The Computer Journal》上,由P. J. Landin在《The mechanical evaluation of expressions》一文中提出了applicative expression和closure的概念。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9b/0c/9b6c6693afe654b4cfdbf16852b82a0c.png" alt="">
|
||||
|
||||
在上世纪60年代,主流的编程语言是基于lambda演算的函数式编程语言,所以这个最初的闭包定义,使用了大量的函数式术语。一个不太精确的描述是“带有一系列信息的λ表达式”。对函数式语言而言,λ表达式其实就是函数。
|
||||
|
||||
我们可以这样简单理解一下,闭包其实只是一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境。
|
||||
|
||||
这个古典的闭包定义中,闭包包含两个部分。
|
||||
|
||||
<li>环境部分
|
||||
<ul>
|
||||
- 环境
|
||||
- 标识符列表
|
||||
|
||||
当我们把视角放在JavaScript的标准中,我们发现,标准中并没有出现过closure这个术语,但是,我们却不难根据古典定义,在JavaScript中找到对应的闭包组成部分。
|
||||
|
||||
<li>环境部分
|
||||
<ul>
|
||||
- 环境:函数的词法环境(执行上下文的一部分)
|
||||
- 标识符列表:函数中用到的未声明的变量
|
||||
|
||||
至此,我们可以认为,JavaScript中的函数完全符合闭包的定义。它的环境部分是函数词法环境部分组成,它的标识符列表是函数中用到的未声明变量,它的表达式部分就是函数体。
|
||||
|
||||
这里我们容易产生一个常见的概念误区,有些人会把JavaScript执行上下文,或者作用域(Scope,ES3中规定的执行上下文的一部分)这个概念当作闭包。
|
||||
|
||||
实际上JavaScript中跟闭包对应的概念就是“函数”,可能是这个概念太过于普通,跟闭包看起来又没什么联系,所以大家才不自觉地把这个概念对应到了看起来更特别的“作用域”吧(其实我早年也是这么理解闭包,直到后来被朋友纠正,查了资料才改正过来)。
|
||||
|
||||
### 执行上下文:执行的基础设施
|
||||
|
||||
相比普通函数,JavaScript函数的主要复杂性来自于它携带的“环境部分”。当然,发展到今天的JavaScript,它所定义的环境部分,已经比当初经典的定义复杂了很多。
|
||||
|
||||
JavaScript中与闭包“环境部分”相对应的术语是“词法环境”,但是JavaScript函数比λ函数要复杂得多,我们还要处理this、变量声明、with等等一系列的复杂语法,λ函数中可没有这些东西,所以,在JavaScript的设计中,词法环境只是JavaScript执行上下文的一部分。
|
||||
|
||||
JavaScript标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文”。
|
||||
|
||||
因为这部分术语经历了比较多的版本和社区的演绎,所以定义比较混乱,这里我们先来理一下JavaScript中的概念。
|
||||
|
||||
**执行上下文在ES3中**,包含三个部分。
|
||||
|
||||
- scope:作用域,也常常被叫做作用域链。
|
||||
- variable object:变量对象,用于存储变量的对象。
|
||||
- this value:this值。
|
||||
|
||||
**在ES5中**,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。
|
||||
|
||||
- lexical environment:词法环境,当获取变量时使用。
|
||||
- variable environment:变量环境,当声明变量时使用。
|
||||
- this value:this值。
|
||||
|
||||
**在ES2018中**,执行上下文又变成了这个样子,this值被归入lexical environment,但是增加了不少内容。
|
||||
|
||||
- lexical environment:词法环境,当获取变量或者this值时使用。
|
||||
- variable environment:变量环境,当声明变量时使用。
|
||||
- code evaluation state:用于恢复代码执行位置。
|
||||
- Function:执行的任务是函数时使用,表示正在被执行的函数。
|
||||
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
|
||||
- Realm:使用的基础库和内置对象实例。
|
||||
- Generator:仅生成器上下文有这个属性,表示当前生成器。
|
||||
|
||||
我们在这里介绍执行上下文的各个版本定义,是考虑到你可能会从各种网上的文章中接触这些概念,如果不把它们理清楚,我们就很难分辨对错。如果是我们自己使用,我建议统一使用最新的ES2018中规定的术语定义。
|
||||
|
||||
尽管我们介绍了这些定义,但我并不打算按照JavaScript标准的思路,从实现的角度去介绍函数的执行过程,这是不容易被理解的。
|
||||
|
||||
我想试着从代码实例出发,跟你一起推导函数执行过程中需要哪些信息,它们又对应着执行上下文中的哪些部分。
|
||||
|
||||
比如,我们看以下的这段JavaScript代码:
|
||||
|
||||
```
|
||||
var b = {}
|
||||
let c = 1
|
||||
this.a = 2;
|
||||
|
||||
```
|
||||
|
||||
要想正确执行它,我们需要知道以下信息:
|
||||
|
||||
1. var 把 b 声明到哪里;
|
||||
1. b 表示哪个变量;
|
||||
1. b 的原型是哪个对象;
|
||||
1. let 把 c 声明到哪里;
|
||||
1. this 指向哪个对象。
|
||||
|
||||
这些信息就需要执行上下文来给出了,这段代码出现在不同的位置,甚至在每次执行中,会关联到不同的执行上下文,所以,同样的代码会产生不一样的行为。
|
||||
|
||||
在这两篇文章中,我会基本覆盖执行上下文的组成部分,本篇我们先讲var声明与赋值,let,realm三个特性来分析上下文提供的信息,分析执行上下文中提供的信息。
|
||||
|
||||
### var 声明与赋值
|
||||
|
||||
我们来分析一段代码:
|
||||
|
||||
```
|
||||
var b = 1
|
||||
|
||||
```
|
||||
|
||||
通常我们认为它声明了b,并且为它赋值为1,var声明作用域函数执行的作用域。也就是说,var会穿透for 、if等语句。
|
||||
|
||||
在只有var,没有let的旧JavaScript时代,诞生了一个技巧,叫做:立即执行的函数表达式(IIFE),通过创建一个函数,并且立即执行,来构造一个新的域,从而控制var的范围。
|
||||
|
||||
由于语法规定了function关键字开头是函数声明,所以要想让函数变成函数表达式,我们必须得加点东西,最常见的做法是加括号。
|
||||
|
||||
```
|
||||
(function(){
|
||||
var a;
|
||||
//code
|
||||
}());
|
||||
|
||||
|
||||
(function(){
|
||||
var a;
|
||||
//code
|
||||
})();
|
||||
|
||||
```
|
||||
|
||||
但是,括号有个缺点,那就是如果上一行代码不写分号,括号会被解释为上一行代码最末的函数调用,产生完全不符合预期,并且难以调试的行为,加号等运算符也有类似的问题。所以一些推荐不加分号的代码风格规范,会要求在括号前面加上分号。
|
||||
|
||||
```
|
||||
;(function(){
|
||||
var a;
|
||||
//code
|
||||
}())
|
||||
|
||||
|
||||
;(function(){
|
||||
var a;
|
||||
//code
|
||||
})()
|
||||
|
||||
```
|
||||
|
||||
我比较推荐的写法是使用void关键字。也就是下面的这种形式。
|
||||
|
||||
```
|
||||
void function(){
|
||||
var a;
|
||||
//code
|
||||
}();
|
||||
|
||||
```
|
||||
|
||||
这有效避免了语法问题,同时,语义上void运算表示忽略后面表达式的值,变成undefined,我们确实不关心IIFE的返回值,所以语义也更为合理。
|
||||
|
||||
值得特别注意的是,有时候var的特性会导致声明的变量和被赋值的变量是两个b,JavaScript中有特例,那就是使用with的时候:
|
||||
|
||||
```
|
||||
var b;
|
||||
void function(){
|
||||
var env = {b:1};
|
||||
b = 2;
|
||||
console.log("In function b:", b);
|
||||
with(env) {
|
||||
var b = 3;
|
||||
console.log("In with b:", b);
|
||||
}
|
||||
}();
|
||||
console.log("Global b:", b);
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,我们利用立即执行的函数表达式(IIFE)构造了一个函数的执行环境,并且在里面使用了我们一开头的代码。
|
||||
|
||||
可以看到,在Global function with三个环境中,b的值都不一样,而在function环境中,并没有出现var b,这说明with内的var b作用到了function这个环境当中。
|
||||
|
||||
var b = {} 这样一句对两个域产生了作用,从语言的角度是个非常糟糕的设计,这也是一些人坚定地反对在任何场景下使用with的原因之一。
|
||||
|
||||
### let
|
||||
|
||||
let是 ES6开始引入的新的变量声明模式,比起var的诸多弊病,let做了非常明确的梳理和规定。
|
||||
|
||||
为了实现let,JavaScript在运行时引入了块级作用域。也就是说,在let出现之前,JavaScript的 if for 等语句皆不产生作用域。
|
||||
|
||||
我简单统计了下,以下语句会产生let使用的作用域:
|
||||
|
||||
- for;
|
||||
- if;
|
||||
- switch;
|
||||
- try/catch/finally。
|
||||
|
||||
### Realm
|
||||
|
||||
在最新的标准(9.0)中,JavaScript引入了一个新概念Realm,它的中文意思是“国度”“领域”“范围”。这个英文的用法就有点比喻的意思,几个翻译都不太适合JavaScript语境,所以这里就不翻译啦。
|
||||
|
||||
我们继续来看这段代码:
|
||||
|
||||
```
|
||||
var b = {}
|
||||
|
||||
```
|
||||
|
||||
在 ES2016 之前的版本中,标准中甚少提及{}的原型问题。但在实际的前端开发中,通过iframe等方式创建多window环境并非罕见的操作,所以,这才促成了新概念Realm的引入。
|
||||
|
||||
Realm中包含一组完整的内置对象,而且是复制关系。
|
||||
|
||||
对不同Realm中的对象操作,会有一些需要格外注意的问题,比如 instanceOf 几乎是失效的。
|
||||
|
||||
以下代码展示了在浏览器环境中获取来自两个Realm的对象,它们跟本土的Object做instanceOf时会产生差异:
|
||||
|
||||
```
|
||||
var iframe = document.createElement('iframe')
|
||||
document.documentElement.appendChild(iframe)
|
||||
iframe.src="javascript:var b = {};"
|
||||
|
||||
var b1 = iframe.contentWindow.b;
|
||||
var b2 = {};
|
||||
|
||||
console.log(typeof b1, typeof b2); //object object
|
||||
|
||||
console.log(b1 instanceof Object, b2 instanceof Object); //false true
|
||||
|
||||
```
|
||||
|
||||
可以看到,由于b1、 b2由同样的代码“ {} ”在不同的Realm中执行,所以表现出了不同的行为。
|
||||
|
||||
## 结语
|
||||
|
||||
在今天的课程中,我帮你梳理了一些概念:有编程语言的概念闭包,也有各个版本中的JavaScript标准中的概念:执行上下文、作用域、this值等等。
|
||||
|
||||
之后我们又从代码的角度,分析了一些执行上下文中所需要的信息,并从`var`、`let`、对象字面量等语法中,推导出了词法作用域、变量作用域、Realm的设计。
|
||||
|
||||
最后留给你一个问题:你喜欢使用let还是var?听过今天的课程,你的想法是否有改变呢?为什么?
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<audio id="audio" title="JavaScript执行(四):try里面放return,finally还会执行吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/10/62/103aec09ba6db2a2e9c485a4fa5c1462.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
在前面几篇文章中,我们已经了解了关于执行上下文、作用域、闭包之间的关系。
|
||||
|
||||
今天,我们则要说一说更为细节的部分:语句。
|
||||
|
||||
语句是任何编程语言的基础结构,与JavaScript对象一样,JavaScript语句同样具有“看起来很像其它语言,但是其实一点都不一样”的特点。
|
||||
|
||||
我们比较常见的语句包括变量声明、表达式、条件、循环等,这些都是大家非常熟悉的东西,对于它们的行为,我在这里就不赘述了。
|
||||
|
||||
为了了解JavaScript语句有哪些特别之处,首先我们要看一个不太常见的例子,我会通过这个例子,来向你介绍JavaScript语句执行机制涉及的一种基础类型:Completion类型。
|
||||
|
||||
## Completion类型
|
||||
|
||||
我们来看一个例子。在函数foo中,使用了一组try语句。我们可以先来做一个小实验,在try中有return语句,finally中的内容还会执行吗?我们来看一段代码。
|
||||
|
||||
```
|
||||
function foo(){
|
||||
try{
|
||||
return 0;
|
||||
} catch(err) {
|
||||
|
||||
} finally {
|
||||
console.log("a")
|
||||
}
|
||||
}
|
||||
|
||||
console.log(foo());
|
||||
|
||||
```
|
||||
|
||||
通过实际试验,我们可以看到,finally确实执行了,而且return语句也生效了,foo()返回了结果0。
|
||||
|
||||
虽然return执行了,但是函数并没有立即返回,又执行了finally里面的内容,这样的行为违背了很多人的直觉。
|
||||
|
||||
如果在这个例子中,我们在finally中加入return语句,会发生什么呢?
|
||||
|
||||
```
|
||||
function foo(){
|
||||
try{
|
||||
return 0;
|
||||
} catch(err) {
|
||||
|
||||
} finally {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(foo());
|
||||
|
||||
```
|
||||
|
||||
通过实际执行,我们看到,finally中的return “覆盖”了try中的return。在一个函数中执行了两次return,这已经超出了很多人的常识,也是其它语言中不会出现的一种行为。
|
||||
|
||||
面对如此怪异的行为,我们当然可以把它作为一个孤立的知识去记忆,但是实际上,这背后有一套机制在运作。
|
||||
|
||||
这一机制的基础正是JavaScript语句执行的完成状态,我们用一个标准类型来表示:Completion Record(我在类型一节提到过,Completion Record用于描述异常、跳出等语句执行过程)。
|
||||
|
||||
Completion Record 表示一个语句执行完之后的结果,它有三个字段:
|
||||
|
||||
- [[type]] 表示完成的类型,有break continue return throw和normal几种类型;
|
||||
- [[value]] 表示语句的返回值,如果语句没有,则是empty;
|
||||
- [[target]] 表示语句的目标,通常是一个JavaScript标签(标签在后文会有介绍)。
|
||||
|
||||
JavaScript正是依靠语句的 Completion Record类型,方才可以在语句的复杂嵌套结构中,实现各种控制。接下来我们要来了解一下JavaScript使用Completion Record类型,控制语句执行的过程。
|
||||
|
||||
首先我们来看看语句有几种分类。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/98/d5/98ce53be306344c018cddd6c083392d5.jpg" alt="">
|
||||
|
||||
## 普通的语句
|
||||
|
||||
在JavaScript中,我们把不带控制能力的语句称为普通语句。普通语句有下面几种。
|
||||
|
||||
<li>声明类语句
|
||||
<ul>
|
||||
- var声明
|
||||
- const声明
|
||||
- let声明
|
||||
- 函数声明
|
||||
- 类声明
|
||||
|
||||
这些语句在执行时,从前到后顺次执行(我们这里先忽略var和函数声明的预处理机制),没有任何分支或者重复执行逻辑。
|
||||
|
||||
普通语句执行后,会得到 [[type]] 为 normal 的 Completion Record,JavaScript引擎遇到这样的Completion Record,会继续执行下一条语句。
|
||||
|
||||
这些语句中,只有表达式语句会产生 [[value]],当然,从引擎控制的角度,这个value并没有什么用处。
|
||||
|
||||
如果你经常使用Chrome自带的调试工具,可以知道,输入一个表达式,在控制台可以得到结果,但是在前面加上var,就变成了undefined。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a3/67/a35801b1b82654d17e413e51b340d767.png" alt="">
|
||||
|
||||
Chrome控制台显示的正是语句的Completion Record的[[value]]。
|
||||
|
||||
## 语句块
|
||||
|
||||
介绍完了普通语句,我们再来介绍一个比较特殊的语句:语句块。
|
||||
|
||||
语句块就是拿大括号括起来的一组语句,它是一种语句的复合结构,可以嵌套。
|
||||
|
||||
语句块本身并不复杂,我们需要注意的是语句块内部的语句的Completion Record的[[type]] 如果不为 normal,会打断语句块后续的语句执行。
|
||||
|
||||
比如我们考虑,一个[[type]]为return的语句,出现在一个语句块中的情况。
|
||||
|
||||
从语句的这个type中,我们大概可以猜到它由哪些特定语句产生,我们就来说说最开始的例子中的 return。
|
||||
|
||||
return语句可能产生return或者throw类型的Completion Record。我们来看一个例子。
|
||||
|
||||
先给出一个内部为普通语句的语句块:
|
||||
|
||||
```
|
||||
{
|
||||
var i = 1; // normal, empty, empty
|
||||
i ++; // normal, 1, empty
|
||||
console.log(i) //normal, undefined, empty
|
||||
} // normal, undefined, empty
|
||||
|
||||
```
|
||||
|
||||
在每一行的注释中,我给出了语句的Completion Record。
|
||||
|
||||
我们看到,在一个block中,如果每一个语句都是normal类型,那么它会顺次执行。接下来我们加入return试试看。
|
||||
|
||||
```
|
||||
{
|
||||
var i = 1; // normal, empty, empty
|
||||
return i; // return, 1, empty
|
||||
i ++;
|
||||
console.log(i)
|
||||
} // return, 1, empty
|
||||
|
||||
```
|
||||
|
||||
但是假如我们在block中插入了一条return语句,产生了一个非normal记录,那么整个block会成为非normal。这个结构就保证了非normal的完成类型可以穿透复杂的语句嵌套结构,产生控制效果。
|
||||
|
||||
接下来我们就具体讲讲控制类语句。
|
||||
|
||||
## 控制型语句
|
||||
|
||||
控制型语句带有 if、switch关键字,它们会对不同类型的Completion Record产生反应。
|
||||
|
||||
控制类语句分成两部分,一类是对其内部造成影响,如if、switch、while/for、try。
|
||||
|
||||
另一类是对外部造成影响如break、continue、return、throw,这两类语句的配合,会产生控制代码执行顺序和执行逻辑的效果,这也是我们编程的主要工作。
|
||||
|
||||
一般来说, for/while - break/continue 和 try - throw 这样比较符合逻辑的组合,是大家比较熟悉的,但是,实际上,我们需要控制语句跟break 、continue 、return 、throw四种类型与控制语句两两组合产生的效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/d3/7760027d7ee09bdc8ec140efa9caf1d3.png" alt="">
|
||||
|
||||
通过这个表,我们不难发现知识的盲点,也就是我们最初的的case中的try和return的组合了。
|
||||
|
||||
因为finally中的内容必须保证执行,所以 try/catch执行完毕,即使得到的结果是非normal型的完成记录,也必须要执行finally。
|
||||
|
||||
而当finally执行也得到了非normal记录,则会使finally中的记录作为整个try结构的结果。
|
||||
|
||||
## 带标签的语句
|
||||
|
||||
前文我重点讲了type在语句控制中的作用,接下来我们重点来讲一下最后一个字段:target,这涉及了JavaScript中的一个语法,带标签的语句。
|
||||
|
||||
实际上,任何JavaScript语句是可以加标签的,在语句前加冒号即可:
|
||||
|
||||
```
|
||||
firstStatement: var i = 1;
|
||||
|
||||
```
|
||||
|
||||
大部分时候,这个东西类似于注释,没有任何用处。唯一有作用的时候是:与完成记录类型中的target相配合,用于跳出多层循环。
|
||||
|
||||
```
|
||||
outer: while(true) {
|
||||
inner: while(true) {
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
console.log("finished")
|
||||
|
||||
```
|
||||
|
||||
break/continue 语句如果后跟了关键字,会产生带target的完成记录。一旦完成记录带了target,那么只有拥有对应label的循环语句会消费它。
|
||||
|
||||
## 结语
|
||||
|
||||
我们以Completion Record类型为线索,为你讲解了JavaScript语句执行的原理。
|
||||
|
||||
因为JavaScript语句存在着嵌套关系,所以执行过程实际上主要在一个树形结构上进行, 树形结构的每一个节点执行后产生Completion Record,根据语句的结构和Completion Record,JavaScript实现了各种分支和跳出逻辑。
|
||||
|
||||
你遇到哪些语句中的执行的实际效果,是跟你想象的有所出入呢,你可以给我留言,我们一起讨论。
|
||||
|
||||
|
||||
366
极客时间专栏/重学前端/模块一:JavaScript/JavaScript类型:关于类型,有哪些你不知道的细节?.md
Normal file
366
极客时间专栏/重学前端/模块一:JavaScript/JavaScript类型:关于类型,有哪些你不知道的细节?.md
Normal file
@@ -0,0 +1,366 @@
|
||||
<audio id="audio" title="JavaScript类型:关于类型,有哪些你不知道的细节?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5d/02/5d547304bd595d7ae180de74d8c48302.mp3"></audio>
|
||||
|
||||
你好,我是winter。今天我们来讲讲JavaScript的内容,在这个部分,我首先想跟你聊一聊类型。
|
||||
|
||||
JavaScript类型对每个前端程序员来说,几乎都是最为熟悉的概念了。但是你真的很了解它们吗?我们不妨来看看下面的几个问题。
|
||||
|
||||
- 为什么有的编程规范要求用void 0代替undefined?
|
||||
- 字符串有最大长度吗?
|
||||
- 0.1 + 0.2不是等于0.3么?为什么JavaScript里不是这样的?
|
||||
- ES6新加入的Symbol是个什么东西?
|
||||
- 为什么给对象添加的方法能用在基本类型上?
|
||||
|
||||
如果你答起来还有些犹豫的地方,这就说明你对这部分知识点,还是有些遗漏之处的。没关系,今天我来帮你一一补上。
|
||||
|
||||
我在前面提到过,我们的JavaScript模块会从运行时、文法和执行过程三个角度去剖析JS的知识体系,本篇我们就从运行时的角度去看JavaScript的类型系统。
|
||||
|
||||
>
|
||||
运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于7个类型之一。从变量、参数、返回值到表达式中间结果,任何JavaScript代码运行过程中产生的数据,都具有运行时类型。
|
||||
|
||||
|
||||
## 类型
|
||||
|
||||
JavaScript语言的每一个值都属于某一种数据类型。JavaScript语言规定了7种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。根据最新的语言标准,这7种语言类型是:
|
||||
|
||||
1. Undefined;
|
||||
1. Null;
|
||||
1. Boolean;
|
||||
1. String;
|
||||
1. Number;
|
||||
1. Symbol;
|
||||
1. Object。
|
||||
|
||||
除了ES6中新加入的Symbol类型,剩下6种类型都是我们日常开发中的老朋友了,但是,要想回答文章一开始的问题,我们需要重新认识一下这些老朋友,下面我们就来从简单到复杂,重新学习一下这些类型。
|
||||
|
||||
## Undefined、Null
|
||||
|
||||
我们的第一个问题,为什么有的编程规范要求用void 0代替undefined?现在我们就分别来看一下。
|
||||
|
||||
Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined,一般我们可以用全局变量undefined(就是名为undefined的这个变量)来表达这个值,或者 void 运算来把任意一个表达式变成 undefined 值。
|
||||
|
||||
但是呢,因为JavaScript的代码undefined是一个变量,而并非是一个关键字,这是JavaScript语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用 void 0 来获取undefined值。
|
||||
|
||||
Undefined跟 Null 有一定的表意差别,Null表示的是:“定义了但是为空”。所以,在实际编程时,我们一般不会把变量赋值为 undefined,这样可以保证所有值为 undefined 的变量,都是从未赋值的自然状态。
|
||||
|
||||
Null 类型也只有一个值,就是 null,它的语义表示空值,与 undefined 不同,null 是 JavaScript 关键字,所以在任何代码中,你都可以放心用 null 关键字来获取 null 值。
|
||||
|
||||
## Boolean
|
||||
|
||||
Boolean 类型有两个值, true 和 false,它用于表示逻辑意义上的真和假,同样有关键字 true 和 false 来表示两个值。这个类型很简单,我就不做过多介绍了。
|
||||
|
||||
## String
|
||||
|
||||
我们来看看字符串是否有最大长度。
|
||||
|
||||
String 用于表示文本数据。String 有最大长度是 2^53 - 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解中的字符数。
|
||||
|
||||
因为String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。
|
||||
|
||||
>
|
||||
Note:现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF是Unicode的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。
|
||||
|
||||
|
||||
JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
|
||||
|
||||
JavaScript 字符串把每个 UTF16 单元当作一个字符来处理,所以处理非BMP(超出 U+0000 - U+FFFF 范围)的字符时,你应该格外小心。
|
||||
|
||||
JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。
|
||||
|
||||
## Number
|
||||
|
||||
下面,我们来说说Number类型。Number类型表示我们通常意义上的“数字”。这个数字大致对应数学中的有理数,当然,在计算机中,我们有一定的精度限制。
|
||||
|
||||
JavaScript中的Number类型有 18437736874454810627(即2^64-2^53+3) 个值。
|
||||
|
||||
JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是JavaScript为了表达几个额外的语言场景(比如不让除以0出错,而引入了无穷大的概念),规定了几个例外情况:
|
||||
|
||||
- NaN,占用了 9007199254740990,这原本是符合IEEE规则的数字;
|
||||
- Infinity,无穷大;
|
||||
- -Infinity,负无穷大。
|
||||
|
||||
另外,值得注意的是,JavaScript中有 +0 和 -0,在加法类运算中它们没有区别,但是除法的场合则需要特别留意区分,“忘记检测除以-0,而得到负无穷大”的情况经常会导致错误,而区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。
|
||||
|
||||
根据双精度浮点数的定义,Number类型中有效的整数范围是-0x1fffffffffffff至0x1fffffffffffff,所以Number无法精确表示此范围外的整数。
|
||||
|
||||
同样根据浮点数的定义,非整数的Number类型无法用 ==(===也不行) 来比较,一段著名的代码,这也正是我们第三题的问题,为什么在JavaScript中,0.1+0.2不能=0.3:
|
||||
|
||||
```
|
||||
console.log( 0.1 + 0.2 == 0.3);
|
||||
|
||||
```
|
||||
|
||||
这里输出的结果是false,说明两边不相等的,这是浮点运算的特点,也是很多同学疑惑的来源,浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。
|
||||
|
||||
所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用JavaScript提供的最小精度值:
|
||||
|
||||
```
|
||||
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
|
||||
|
||||
```
|
||||
|
||||
检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。
|
||||
|
||||
## Symbol
|
||||
|
||||
Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象key的集合,在ES6规范中,整个对象系统被用Symbol 重塑。
|
||||
|
||||
在后面的文章中,我会详细叙述 Symbol 跟对象系统。这里我们只介绍Symbol类型本身:它有哪些部分,它表示什么意思,以及如何创建Symbol类型。
|
||||
|
||||
Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol也不相等。
|
||||
|
||||
我们创建 Symbol 的方式是使用全局的 Symbol 函数。例如:
|
||||
|
||||
```
|
||||
var mySymbol = Symbol("my symbol");
|
||||
|
||||
```
|
||||
|
||||
一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为:
|
||||
|
||||
```
|
||||
var o = new Object
|
||||
|
||||
o[Symbol.iterator] = function() {
|
||||
var v = 0
|
||||
return {
|
||||
next: function() {
|
||||
return { value: v++, done: v > 10 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for(var v of o)
|
||||
console.log(v); // 0 1 2 3 ... 9
|
||||
|
||||
```
|
||||
|
||||
代码中我们定义了iterator之后,用for(var v of o)就可以调用这个函数,然后我们可以根据函数的行为,产生一个for…of的行为。
|
||||
|
||||
这里我们给对象o添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个0到10的迭代器,之后我们就可以在for of中愉快地使用这个o对象啦。
|
||||
|
||||
这些标准中被称为“众所周知”的 Symbol,也构成了语言的一类接口形式。它们允许编写与语言结合更紧密的 API。
|
||||
|
||||
## Object
|
||||
|
||||
Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。Object表示对象的意思,它是一切有形和无形物体的总称。
|
||||
|
||||
下面我们来看一看,为什么给对象添加的方法能用在基本类型上?
|
||||
|
||||
在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是key-value结构,key可以是字符串或者 Symbol类型。
|
||||
|
||||
关于对象的机制,后面会有单独的一篇来讲述,这里我重点从类型的角度来介绍对象类型。
|
||||
|
||||
提到对象,我们必须要提到一个概念:类。
|
||||
|
||||
因为 C++ 和 Java 的成功,在这两门语言中,每个类都是一个类型,二者几乎等同,以至于很多人常常会把JavaScript的“类”与类型混淆。
|
||||
|
||||
事实上,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而JavaScript中是无法自定义类型的。
|
||||
|
||||
JavaScript中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:
|
||||
|
||||
- Number;
|
||||
- String;
|
||||
- Boolean;
|
||||
- Symbol。
|
||||
|
||||
所以,我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。
|
||||
|
||||
Number、String和Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。
|
||||
|
||||
Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。
|
||||
|
||||
JavaScript 语言设计上试图模糊对象和基本类型之间的关系,我们日常代码可以把对象的方法在基本类型上使用,比如:
|
||||
|
||||
```
|
||||
console.log("abc".charAt(0)); //a
|
||||
|
||||
```
|
||||
|
||||
甚至我们在原型上添加方法,都可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了hello方法,在任何 Symbol 类型变量都可以调用。
|
||||
|
||||
```
|
||||
Symbol.prototype.hello = () => console.log("hello");
|
||||
|
||||
var a = Symbol("a");
|
||||
console.log(typeof a); //symbol,a并非对象
|
||||
a.hello(); //hello,有效
|
||||
|
||||
```
|
||||
|
||||
所以我们文章开头的问题,答案就是`.` 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。
|
||||
|
||||
## 类型转换
|
||||
|
||||
讲完了基本类型,我们来介绍一个现象:类型转换。
|
||||
|
||||
因为JS是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。
|
||||
|
||||
其中最为臭名昭著的是JavaScript中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。
|
||||
|
||||
这里我们当然也不打算讲解==的规则,它属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“ ==”,而要求程序员进行显式地类型转换后,用 === 比较。
|
||||
|
||||
其它运算,如加减乘除大于小于,也都会涉及类型转换。幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/71/20/71bafbd2404dc3ffa5ccf5d0ba077720.jpg" alt="">
|
||||
|
||||
在这个里面,较为复杂的部分是Number和String之间的转换,以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。
|
||||
|
||||
### StringToNumber
|
||||
|
||||
字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:
|
||||
|
||||
- 30;
|
||||
- 0b111;
|
||||
- 0o13;
|
||||
- 0xFF。
|
||||
|
||||
此外,JavaScript支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的e来表示:
|
||||
|
||||
- 1e3;
|
||||
- -1e-2。
|
||||
|
||||
需要注意的是,parseInt 和 parseFloat 并不使用这个转换,所以支持的语法跟这里不尽相同。
|
||||
|
||||
在不传入第二个参数的情况下,parseInt只支持16进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。
|
||||
|
||||
在一些古老的浏览器环境中,parseInt还支持0开头的数字作为8进制前缀,这是很多错误的来源。所以在任何环境下,都建议传入parseInt的第二个参数,而parseFloat则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。
|
||||
|
||||
多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。
|
||||
|
||||
### NumberToString
|
||||
|
||||
在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。当Number绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。这个算法细节繁多,我们从感性的角度认识,它其实就是保证了产生的字符串不会过长。
|
||||
|
||||
具体的算法,你可以去参考JavaScript的语言标准。由于这个部分内容,我觉得在日常开发中很少用到,所以这里我就不去详细地讲解了。
|
||||
|
||||
### 装箱转换
|
||||
|
||||
每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。
|
||||
|
||||
前文提到,全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的call方法来强迫产生装箱。
|
||||
|
||||
我们定义一个函数,函数里面只有return this,然后我们调用函数的call方法到一个Symbol类型的值上,这样就会产生一个symbolObject。
|
||||
|
||||
我们可以用console.log看一下这个东西的type of,它的值是object,我们使用symbolObject instanceof 可以看到,它是Symbol这个类的实例,我们找它的constructor也是等于Symbol的,所以我们无论从哪个角度看,它都是Symbol装箱过的对象:
|
||||
|
||||
```
|
||||
var symbolObject = (function(){ return this; }).call(Symbol("a"));
|
||||
|
||||
console.log(typeof symbolObject); //object
|
||||
console.log(symbolObject instanceof Symbol); //true
|
||||
console.log(symbolObject.constructor == Symbol); //true
|
||||
|
||||
```
|
||||
|
||||
装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。
|
||||
|
||||
使用内置的 Object 函数,我们可以在JavaScript代码中显式调用装箱能力。
|
||||
|
||||
```
|
||||
var symbolObject = Object(Symbol("a"));
|
||||
|
||||
console.log(typeof symbolObject); //object
|
||||
console.log(symbolObject instanceof Symbol); //true
|
||||
console.log(symbolObject.constructor == Symbol); //true
|
||||
|
||||
```
|
||||
|
||||
每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:
|
||||
|
||||
```
|
||||
var symbolObject = Object(Symbol("a"));
|
||||
|
||||
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]
|
||||
|
||||
```
|
||||
|
||||
在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。
|
||||
|
||||
但需要注意的是,call本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。
|
||||
|
||||
### 拆箱转换
|
||||
|
||||
在JavaScript标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。
|
||||
|
||||
对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。
|
||||
|
||||
拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。
|
||||
|
||||
```
|
||||
var o = {
|
||||
valueOf : () => {console.log("valueOf"); return {}},
|
||||
toString : () => {console.log("toString"); return {}}
|
||||
}
|
||||
|
||||
o * 2
|
||||
// valueOf
|
||||
// toString
|
||||
// TypeError
|
||||
|
||||
```
|
||||
|
||||
我们定义了一个对象o,o有valueOf和toString两个方法,这两个方法都返回一个对象,然后我们进行o*2这个运算的时候,你会看见先执行了valueOf,接下来是toString,最后抛出了一个TypeError,这就说明了这个拆箱转换失败了。
|
||||
|
||||
到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从o*2换成 String(o),那么你会看到调用顺序就变了。
|
||||
|
||||
```
|
||||
var o = {
|
||||
valueOf : () => {console.log("valueOf"); return {}},
|
||||
toString : () => {console.log("toString"); return {}}
|
||||
}
|
||||
|
||||
String(o)
|
||||
// toString
|
||||
// valueOf
|
||||
// TypeError
|
||||
|
||||
```
|
||||
|
||||
在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。
|
||||
|
||||
```
|
||||
var o = {
|
||||
valueOf : () => {console.log("valueOf"); return {}},
|
||||
toString : () => {console.log("toString"); return {}}
|
||||
}
|
||||
|
||||
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
|
||||
|
||||
|
||||
console.log(o + "")
|
||||
// toPrimitive
|
||||
// hello
|
||||
|
||||
```
|
||||
|
||||
## 结语
|
||||
|
||||
在本篇文章中,我们介绍了 JavaScript 运行时的类型系统。这里回顾一下今天讲解的知识点。
|
||||
|
||||
除了这七种语言类型,还有一些语言的实现者更关心的规范类型。
|
||||
|
||||
- List 和 Record: 用于描述函数传参过程。
|
||||
- Set:主要用于解释字符集等。
|
||||
- Completion Record:用于描述异常、跳出等语句执行过程。
|
||||
- Reference:用于描述对象属性访问、delete等。
|
||||
- Property Descriptor:用于描述对象的属性。
|
||||
- Lexical Environment 和 Environment Record:用于描述变量和作用域。
|
||||
- Data Block:用于描述二进制数据。
|
||||
|
||||
有一个说法是:程序 = 算法 + 数据结构,运行时类型包含了所有 JavaScript 执行时所需要的数据结构的定义,所以我们要对它格外重视。
|
||||
|
||||
最后我们留一个实践问题,如果我们不用原生的Number和parseInt,用JavaScript代码实现String到Number的转换,该怎么做呢?请你把自己的代码留言给我吧!
|
||||
|
||||
## 补充阅读
|
||||
|
||||
事实上,“类型”在 JavaScript 中是一个有争议的概念。一方面,标准中规定了运行时数据类型; 另一方面,JavaScript语言中提供了 typeof 这样的运算,用来返回操作数的类型,但 typeof 的运算结果,与运行时类型的规定有很多不一致的地方。我们可以看下表来对照一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/6b/ec4299a73fb84c732efcd360fed6e16b.png" alt="">
|
||||
|
||||
在表格中,多数项是对应的,但是请注意object——Null和function——Object是特例,我们理解类型的时候需要特别注意这个区别。
|
||||
|
||||
从一般语言使用者的角度来看,毫无疑问,我们应该按照 typeof 的结果去理解语言的类型系统。但JavaScript之父本人也在多个场合表示过,typeof 的设计是有缺陷的,只是现在已经错过了修正它的时机。
|
||||
|
||||
# 猜你喜欢
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/1a/08/1a49758821bdbdf6f0a8a1dc5bf39f08.jpg" alt="unpreview">](https://time.geekbang.org/course/intro/163?utm_term=zeusMTA7L&utm_source=app&utm_medium=chongxueqianduan&utm_campaign=163-presell)
|
||||
316
极客时间专栏/重学前端/模块一:JavaScript/JavaScript词法:为什么12.toString会报错?.md
Normal file
316
极客时间专栏/重学前端/模块一:JavaScript/JavaScript词法:为什么12.toString会报错?.md
Normal file
@@ -0,0 +1,316 @@
|
||||
<audio id="audio" title="JavaScript词法:为什么12.toString会报错?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/85/ff/85e235c2494e79efda7deeffd6046bff.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
在前面的文章中,我们已经从运行时的角度了解过JavaScript的知识内容,在接下来的几节课,我们来了解一下JavaScript的文法部分。
|
||||
|
||||
文法是编译原理中对语言的写法的一种规定,一般来说,文法分成词法和语法两种。
|
||||
|
||||
词法规定了语言的最小语义单元:token,可以翻译成“标记”或者“词”,在我的专栏文章中,我统一把token翻译成词。
|
||||
|
||||
从字符到词的整个过程是没有结构的,只要符合词的规则,就构成词,一般来说,词法设计不会包含冲突。词法分析技术上可以使用状态机或者正则表达式来进行,我们的课程主要是学习词法,关于它们实现的细节就不多谈了。
|
||||
|
||||
## 概述
|
||||
|
||||
我们先来看一看JavaScript的词法定义。JavaScript源代码中的输入可以这样分类:
|
||||
|
||||
- WhiteSpace 空白字符
|
||||
- LineTerminator 换行符
|
||||
- Comment 注释
|
||||
<li>Token 词
|
||||
<ul>
|
||||
- IdentifierName 标识符名称,典型案例是我们使用的变量名,注意这里关键字也包含在内了。
|
||||
- Punctuator 符号,我们使用的运算符和大括号等符号。
|
||||
- NumericLiteral 数字直接量,就是我们写的数字。
|
||||
- StringLiteral 字符串直接量,就是我们用单引号或者双引号引起来的直接量。
|
||||
- Template 字符串模板,用反引号``` 括起来的直接量。
|
||||
|
||||
这个设计符合比较通用的编程语言设计方式,不过,JavaScript中有一些特别之处,我下面就来讲讲特别在哪里。
|
||||
|
||||
首先是除法和正则表达式冲突问题。我们都知道,JavaScript不但支持除法运算符“ / ”和“ /= ”,还支持用斜杠括起来的正则表达式“ /abc/ ”。
|
||||
|
||||
但是,这时候对词法分析来说,其实是没有办法处理的,所以JavaScript的解决方案是定义两组词法,然后靠语法分析传一个标志给词法分析器,让它来决定使用哪一套词法。
|
||||
|
||||
JavaScript词法的另一个特别设计是字符串模板,模板语法大概是这样的:
|
||||
|
||||
```
|
||||
`Hello, ${name}`
|
||||
|
||||
```
|
||||
|
||||
理论上,“ ${ } ”内部可以放任何JavaScript表达式代码,而这些代码是以“ } ” 结尾的,也就是说,这部分词法不允许出现“ } ”运算符。
|
||||
|
||||
是否允许“ } ”的两种情况,与除法和正则表达式的两种情况相乘就是四种词法定义,所以你在JavaScript标准中,可以看到四种定义:
|
||||
|
||||
- InputElementDiv;
|
||||
- InputElementRegExp;
|
||||
- InputElementRegExpOrTemplateTail;
|
||||
- InputElementTemplateTail。
|
||||
|
||||
为了解决这两个问题,标准中还不得不把除法、正则表达式直接量和“ } ”从token中单独抽出来,用词上,也把原本的 Token 改为 CommonToken。
|
||||
|
||||
但是我认为,从理解的角度上出发,我们不应该受到影响,所以在本课,我们依然把它们归类到token来理解。
|
||||
|
||||
对一般的语言的词法分析过程来说,都会丢弃除了token之外的输入,但是对JavaScript来说,不太一样,换行符和注释还会影响语法分析过程,这个我们将会在语法部分给你详细讲解(所以要实现JavaScript的解释器,词法分析和语法分析非常麻烦,需要来回传递信息)。
|
||||
|
||||
接下来我来给你详细介绍一下。
|
||||
|
||||
### 空白符号 Whitespace
|
||||
|
||||
说起空白符号,想必给大家留下的印象就是空格,但是实际上,JavaScript可以支持更多空白符号。
|
||||
|
||||
<li>
|
||||
`<HT>`(或称`<TAB>`)是U+0009,是缩进TAB符,也就是字符串中写的 \t 。
|
||||
</li>
|
||||
<li>
|
||||
`<VT>`是U+000B,也就是垂直方向的TAB符 \v,这个字符在键盘上很难打出来,所以很少用到。
|
||||
</li>
|
||||
<li>
|
||||
`<FF>`是U+000C,Form Feed,分页符,字符串直接量中写作 \f ,现代已经很少有打印源程序的事情发生了,所以这个字符在JavaScript源代码中很少用到。
|
||||
</li>
|
||||
<li>
|
||||
`<SP>`是U+0020,就是最普通的空格了。
|
||||
</li>
|
||||
<li>
|
||||
`<NBSP>`是U+00A0,非断行空格,它是SP的一个变体,在文字排版中,可以避免因为空格在此处发生断行,其它方面和普通空格完全一样。多数的JavaScript编辑环境都会把它当做普通空格(因为一般源代码编辑环境根本就不会自动折行……)。HTML中,很多人喜欢用的 `&nbsp;` 最后生成的就是它了。
|
||||
</li>
|
||||
<li>
|
||||
`<ZWNBSP>`(旧称`<BOM>`)是U+FEFF,这是ES5新加入的空白符,是Unicode中的零宽非断行空格,在以UTF格式编码的文件中,常常在文件首插入一个额外的U+FEFF,解析UTF文件的程序可以根据U+FEFF的表示方法猜测文件采用哪种UTF编码方式。这个字符也叫做“bit order mark”。
|
||||
</li>
|
||||
|
||||
此外,JavaScript支持所有的Unicode中的空格分类下的空格,我们可以看下表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dd/60/dd26aa9599b61d26e7de807dee2c6360.png" alt="">
|
||||
|
||||
很多公司的编码规范要求JavaScript源代码控制在ASCII范围内,那么,就只有`<TAB>` `<VT>` `<FF>` `<SP>` `<NBSP>`五种空白可用了。
|
||||
|
||||
### 换行符 LineTerminator
|
||||
|
||||
接下来我们来看看换行符,JavaScript中只提供了4种字符作为换行符。
|
||||
|
||||
- `<LF>`
|
||||
- `<CR>`
|
||||
- `<LS>`
|
||||
- `<PS>`
|
||||
|
||||
其中,`<LF>`是U+000A,就是最正常换行符,在字符串中的`\n`。
|
||||
|
||||
`<CR>`是U+000D,这个字符真正意义上的“回车”,在字符串中是`\r`,在一部分Windows风格文本编辑器中,换行是两个字符`\r\n`。
|
||||
|
||||
`<LS>`是U+2028,是Unicode中的行分隔符。`<PS>`是U+2029,是Unicode中的段落分隔符。
|
||||
|
||||
大部分LineTerminator在被词法分析器扫描出之后,会被语法分析器丢弃,但是换行符会影响JavaScript的两个重要语法特性:自动插入分号和“no line terminator”规则。
|
||||
|
||||
### 注释 Comment
|
||||
|
||||
JavaScript的注释分为单行注释和多行注释两种:
|
||||
|
||||
```
|
||||
/* MultiLineCommentChars */
|
||||
// SingleLineCommentChars
|
||||
|
||||
```
|
||||
|
||||
多行注释中允许自由地出现`MultiLineNotAsteriskChar`,也就是除了`*`之外的所有字符。而每一个`*`之后,不能出现正斜杠符`/`。
|
||||
|
||||
除了四种LineTerminator之外,所有字符都可以作为单行注释。
|
||||
|
||||
我们需要注意,多行注释中是否包含换行符号,会对JavaScript语法产生影响,对于“no line terminator”规则来说,带换行的多行注释与换行符是等效的。
|
||||
|
||||
## 标识符名称 IdentifierName
|
||||
|
||||
`IdentifierName`可以以美元符“`$`”、下划线“`_`”或者Unicode字母开始,除了开始字符以外,`IdentifierName`中还可以使用Unicode中的连接标记、数字、以及连接符号。
|
||||
|
||||
`IdentifierName`的任意字符可以使用JavaScript的Unicode转义写法,使用Unicode转义写法时,没有任何字符限制。
|
||||
|
||||
`IdentifierName`可以是`Identifier`、`NullLiteral`、`BooleanLiteral`或者`keyword`,在`ObjectLiteral`中,`IdentifierName`还可以被直接当做属性名称使用。
|
||||
|
||||
仅当不是保留字的时候,`IdentifierName`会被解析为`Identifier`。
|
||||
|
||||
注意`<ZWNJ>`和`<ZWJ>`是ES5新加入的两个格式控制字符,它们都是0宽的。
|
||||
|
||||
我在前面提到了,关键字也属于这个部分,在JavaScript中,关键字有:
|
||||
|
||||
```
|
||||
await break case catch class const continue debugger default delete do else export extends finally for function if import instance of new return super switch this throw try typeof var void while with yield
|
||||
|
||||
```
|
||||
|
||||
除了上述的内容之外,还有1个为了未来使用而保留的关键字:
|
||||
|
||||
```
|
||||
enum
|
||||
|
||||
```
|
||||
|
||||
在严格模式下,有一些额外的为未来使用而保留的关键字:
|
||||
|
||||
```
|
||||
implements package protected interface private public
|
||||
|
||||
```
|
||||
|
||||
除了这些之外,`NullLiteral`(`null`)和`BooleanLiteral`(`true false`)也是保留字,不能用于`Identifier`。
|
||||
|
||||
### 符号 Punctuator
|
||||
|
||||
因为前面提到的除法和正则问题, /和/=两个运算符被拆分为DivPunctuator,因为前面提到的字符串模板问题,`}`也被独立拆分。加在一起,所有符号为:
|
||||
|
||||
```
|
||||
{ ( ) [ ] . ... ; , < > <= >= == != === !== + - * % ** ++ -- << >> >>> & | ^ ! ~ && || ? : = += -= *= %= **= <<= >>= >>>= &= |= ^= => / /= }
|
||||
|
||||
```
|
||||
|
||||
### 数字直接量 NumericLiteral
|
||||
|
||||
我们来看看今天标题提出的问题,JavaScript规范中规定的数字直接量可以支持四种写法:十进制数、二进制整数、八进制整数和十六进制整数。
|
||||
|
||||
十进制的Number可以带小数,小数点前后部分都可以省略,但是不能同时省略,我们看几个例子:
|
||||
|
||||
```
|
||||
.01
|
||||
12.
|
||||
12.01
|
||||
|
||||
```
|
||||
|
||||
这都是合法的数字直接量。这里就有一个问题,也是我们标题提出的问题,我们看一段代码:
|
||||
|
||||
```
|
||||
12.toString()
|
||||
|
||||
```
|
||||
|
||||
这时候`12.` 会被当作省略了小数点后面部分的数字,而单独看成一个整体,所以我们要想让点单独成为一个token,就要加入空格,这样写:
|
||||
|
||||
```
|
||||
12 .toString()
|
||||
|
||||
```
|
||||
|
||||
数字直接量还支持科学计数法,例如:
|
||||
|
||||
```
|
||||
10.24E+2
|
||||
10.24e-2
|
||||
10.24e2
|
||||
|
||||
```
|
||||
|
||||
这里e后面的部分,只允许使用整数。当以`0x` `0b` 或者`0o` 开头时,表示特定进制的整数:
|
||||
|
||||
```
|
||||
0xFA
|
||||
0o73
|
||||
0b10000
|
||||
|
||||
```
|
||||
|
||||
上面这几种进制都不支持小数,也不支持科学计数法。
|
||||
|
||||
### 字符串直接量 StringLiteral
|
||||
|
||||
JavaScript中的StringLiteral支持单引号和双引号两种写法。
|
||||
|
||||
```
|
||||
" DoubleStringCharacters "
|
||||
' SingleStringCharacters '
|
||||
|
||||
```
|
||||
|
||||
单双引号的区别仅仅在于写法,在双引号字符串直接量中,双引号必须转义,在单引号字符串直接量中,单引号必须转义。字符串中其他必须转义的字符是`\`和所有换行符。
|
||||
|
||||
JavaScript中支持四种转义形式,还有一种虽然标准没有定义,但是大部分实现都支持的八进制转义。
|
||||
|
||||
第一种是单字符转义。 即一个反斜杠`\`后面跟一个字符这种形式。
|
||||
|
||||
有特别意义的字符包括有`SingleEscapeCharacter`所定义的9种,见下表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/02/75/022c2c77d0a3c846ad0d61b48c4e0e75.png" alt="">
|
||||
|
||||
除了这9种字符、数字、x和u以及所有的换行符之外,其它字符经过`\`转义后都是自身。
|
||||
|
||||
### 正则表达式直接量 RegularExpressionLiteral
|
||||
|
||||
正则表达式由Body和Flags两部分组成,例如:
|
||||
|
||||
```
|
||||
/RegularExpressionBody/g
|
||||
|
||||
```
|
||||
|
||||
其中Body部分至少有一个字符,第一个字符不能是*(因为/*跟多行注释有词法冲突)。
|
||||
|
||||
正则表达式有自己的语法规则,在词法阶段,仅会对它做简单解析。
|
||||
|
||||
正则表达式并非机械地见到`/`就停止,在正则表达式`[ ]`中的`/`就会被认为是普通字符。我们可以看一个例子:
|
||||
|
||||
```
|
||||
/[/]/.test("/");
|
||||
|
||||
```
|
||||
|
||||
除了`\`、`/` 和`[` 三个字符之外,JavaScript正则表达式中的字符都是普通字符。
|
||||
|
||||
用\和一个非换行符可以组成一个转义,`[ ]`中也支持转义。正则表达式中的flag在词法阶段不会限制字符。
|
||||
|
||||
虽然只有ig几个是有效的,但是任何IdentifierPart(Identifier中合法的字符)序列在词法阶段都会被认为是合法的。
|
||||
|
||||
### 字符串模板 Template
|
||||
|
||||
从语法结构上,Template是个整体,其中的 `${ }` 是并列关系。
|
||||
|
||||
但是实际上,在JavaScript词法中,包含 `${ }` 的 Template,是被拆开分析的,如:
|
||||
|
||||
```
|
||||
`a${b}c${d}e`
|
||||
|
||||
```
|
||||
|
||||
它在JavaScript中被认为是:
|
||||
|
||||
```
|
||||
`a${
|
||||
b
|
||||
}c${
|
||||
d
|
||||
}e`
|
||||
|
||||
```
|
||||
|
||||
它被拆成了五个部分:
|
||||
|
||||
- ``a${` 这个被称为模板头
|
||||
- `}c${` 被称为模板中段
|
||||
- `}e`` 被称为模板尾
|
||||
- `b` 和 `d` 都是普通标识符
|
||||
|
||||
实际上,这里的词法分析过程已经跟语法分析深度耦合了。
|
||||
|
||||
不过我们学习的时候,大可不必按照标准和引擎工程师这样去理解,可以认为模板就是一个由反引号括起来的、可以在中间插入代码的字符串。
|
||||
|
||||
模板支持添加处理函数的写法,这时模板的各段会被拆开,传递给函数当参数:
|
||||
|
||||
```
|
||||
function f(){
|
||||
console.log(arguments);
|
||||
}
|
||||
|
||||
var a = "world"
|
||||
f`Hello ${a}!`; // [["Hello", "!"], world]
|
||||
|
||||
```
|
||||
|
||||
模板字符串不需要关心大多数字符的转义,但是至少 `${` 和 ``` 还是需要处理的。
|
||||
|
||||
模板中的转义跟字符串几乎完全一样,都是使用 `\`。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们一起学习JavaScript的词法部分,这部分的内容包括了空白符号、换行符、注释、标识符名称、符号、数字直接量、字符串直接量、正则表达式直接量、字符串模板。掌握词法对我们平时调试代码至关重要。
|
||||
|
||||
最后,给你留一个问题:用零宽空格和零宽连接符、零宽非连接符,写一段好玩的代码。你可以给我留言,我们一起讨论。
|
||||
|
||||
# 猜你喜欢
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/1a/08/1a49758821bdbdf6f0a8a1dc5bf39f08.jpg" alt="unpreview">](https://time.geekbang.org/course/intro/163?utm_term=zeusMTA7L&utm_source=app&utm_medium=chongxueqianduan&utm_campaign=163-presell)
|
||||
@@ -0,0 +1,453 @@
|
||||
<audio id="audio" title="JavaScript语法(一):在script标签写export为什么会抛错?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/17/31/17e095d0ecd3e81fce501ffbd047b531.mp3"></audio>
|
||||
|
||||
你好,我是winter,今天我们进入到语法部分的学习。在讲解具体的语法结构之前,这一堂课我首先要给你介绍一下JavaScript语法的一些基本规则。
|
||||
|
||||
## 脚本和模块
|
||||
|
||||
首先,JavaScript有两种源文件,一种叫做脚本,一种叫做模块。这个区分是在ES6引入了模块机制开始的,在ES5和之前的版本中,就只有一种源文件类型(就只有脚本)。
|
||||
|
||||
脚本是可以由浏览器或者node环境引入执行的,而模块只能由JavaScript代码用import引入执行。
|
||||
|
||||
从概念上,我们可以认为脚本具有主动性的JavaScript代码段,是控制宿主完成一定任务的代码;而模块是被动性的JavaScript代码段,是等待被调用的库。
|
||||
|
||||
我们对标准中的语法产生式做一些对比,不难发现,实际上模块和脚本之间的区别仅仅在于是否包含import 和 export。
|
||||
|
||||
脚本是一种兼容之前的版本的定义,在这个模式下,没有import就不需要处理加载“.js”文件问题。
|
||||
|
||||
现代浏览器可以支持用script标签引入模块或者脚本,如果要引入模块,必须给script标签添加type=“module”。如果引入脚本,则不需要type。
|
||||
|
||||
```
|
||||
<script type="module" src="xxxxx.js"></script>
|
||||
|
||||
```
|
||||
|
||||
这样,就回答了我们标题中的问题,script标签如果不加`type=“module”`,默认认为我们加载的文件是脚本而非模块,如果我们在脚本中写了export,当然会抛错。
|
||||
|
||||
脚本中可以包含语句。模块中可以包含三种内容:import声明,export声明和语句。普通语句我们会在下一课专门给你讲解,下面我们就来讲讲import声明和export声明。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/44/43fdb35c0300e73bb19c143431f50a44.jpg" alt="">
|
||||
|
||||
### import声明
|
||||
|
||||
我们首先来介绍一下import声明,import声明有两种用法,一个是直接import一个模块,另一个是带from的import,它能引入模块里的一些信息。
|
||||
|
||||
```
|
||||
import "mod"; //引入一个模块
|
||||
import v from "mod"; //把模块默认的导出值放入变量v
|
||||
|
||||
```
|
||||
|
||||
直接import一个模块,只是保证了这个模块代码被执行,引用它的模块是无法获得它的任何信息的。
|
||||
|
||||
带from的import意思是引入模块中的一部分信息,可以把它们变成本地的变量。
|
||||
|
||||
带from的import细分又有三种用法,我们可以分别看下例子:
|
||||
|
||||
- `import x from "./a.js"` 引入模块中导出的默认值。
|
||||
- `import {a as x, modify} from "./a.js";` 引入模块中的变量。
|
||||
- `import * as x from "./a.js"` 把模块中所有的变量以类似对象属性的方式引入。
|
||||
|
||||
第一种方式还可以跟后两种组合使用。
|
||||
|
||||
- `import d, {a as x, modify} from "./a.js"`
|
||||
- `import d, * as x from "./a.js"`
|
||||
|
||||
语法要求不带as的默认值永远在最前。注意,这里的变量实际上仍然可以受到原来模块的控制。
|
||||
|
||||
我们看一个例子,假设有两个模块a和b。我们在模块a中声明了变量和一个修改变量的函数,并且把它们导出。我们用b模块导入了变量和修改变量的函数。
|
||||
|
||||
**模块a:**
|
||||
|
||||
```
|
||||
|
||||
export var a = 1;
|
||||
|
||||
export function modify(){
|
||||
a = 2;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
**模块b:**
|
||||
|
||||
```
|
||||
import {a, modify} from "./a.js";
|
||||
|
||||
console.log(a);
|
||||
|
||||
modify();
|
||||
|
||||
console.log(a);
|
||||
|
||||
```
|
||||
|
||||
当我们调用修改变量的函数后,b模块变量也跟着发生了改变。这说明导入与一般的赋值不同,导入后的变量只是改变了名字,它仍然与原来的变量是同一个。
|
||||
|
||||
### export声明
|
||||
|
||||
我们再来说说export声明。与import相对,export声明承担的是导出的任务。
|
||||
|
||||
模块中导出变量的方式有两种,一种是独立使用export声明,另一种是直接在声明型语句前添加export关键字。
|
||||
|
||||
独立使用export声明就是一个export关键字加上变量名列表,例如:
|
||||
|
||||
```
|
||||
export {a, b, c};
|
||||
|
||||
```
|
||||
|
||||
我们也可以直接在声明型语句前添加export关键字,这里的export可以加在任何声明性质的语句之前,整理如下:
|
||||
|
||||
- var
|
||||
- function (含async和generator)
|
||||
- class
|
||||
- let
|
||||
- const
|
||||
|
||||
export还有一种特殊的用法,就是跟default联合使用。export default 表示导出一个默认变量值,它可以用于function和class。这里导出的变量是没有名称的,可以使用`import x from "./a.js"`这样的语法,在模块中引入。
|
||||
|
||||
export default 还支持一种语法,后面跟一个表达式,例如:
|
||||
|
||||
```
|
||||
var a = {};
|
||||
export default a;
|
||||
|
||||
```
|
||||
|
||||
但是,这里的行为跟导出变量是不一致的,这里导出的是值,导出的就是普通变量a的值,以后a的变化与导出的值就无关了,修改变量a,不会使得其他模块中引入的default值发生改变。
|
||||
|
||||
在import语句前无法加入export,但是我们可以直接使用export from语法。
|
||||
|
||||
```
|
||||
export a from "a.js"
|
||||
|
||||
```
|
||||
|
||||
JavaScript引擎除了执行脚本和模块之外,还可以执行函数。而函数体跟脚本和模块有一定的相似之处,所以接下来,给你讲讲函数体的相关知识。
|
||||
|
||||
## 函数体
|
||||
|
||||
执行函数的行为通常是在JavaScript代码执行时,注册宿主环境的某些事件触发的,而执行的过程,就是执行函数体(函数的花括号中间的部分)。
|
||||
|
||||
我们先看一个例子,感性地理解一下:
|
||||
|
||||
```
|
||||
setTimeout(function(){
|
||||
console.log("go go go");
|
||||
}, 10000)
|
||||
|
||||
```
|
||||
|
||||
这段代码通过setTimeout函数注册了一个函数给宿主,当一定时间之后,宿主就会执行这个函数。
|
||||
|
||||
你还记得吗,我们前面已经在运行时这部分讲过,宿主会为这样的函数创建宏任务。
|
||||
|
||||
当我们学习了语法之后,我们可以认为,宏任务中可能会执行的代码包括“脚本(script)”“模块(module)”和“函数体(function body)”。正因为这样的相似性,我们把函数体也放到本课来讲解。
|
||||
|
||||
函数体其实也是一个语句的列表。跟脚本和模块比起来,函数体中的语句列表中多了return语句可以用。
|
||||
|
||||
函数体实际上有四种,下面,我来分别介绍一下。
|
||||
|
||||
- 普通函数体,例如:
|
||||
|
||||
```
|
||||
function foo(){
|
||||
//Function body
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- 异步函数体,例如:
|
||||
|
||||
```
|
||||
async function foo(){
|
||||
//Function body
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- 生成器函数体,例如:
|
||||
|
||||
```
|
||||
function *foo(){
|
||||
//Function body
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- 异步生成器函数体,例如:
|
||||
|
||||
```
|
||||
async function *foo(){
|
||||
//Function body
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面四种函数体的区别在于:能否使用await或者yield语句。
|
||||
|
||||
关于函数体、模块和脚本能使用的语句,我整理了一个表格,你可以参考一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0b/50/0b24e78625beb70e3346aad1e8cfff50.jpg" alt="">
|
||||
|
||||
讲完了三种语法结构,我再来介绍两个JavaScript语法的全局机制:预处理和指令序言。
|
||||
|
||||
这两个机制对于我们解释一些JavaScript的语法现象非常重要。不理解预处理机制我们就无法理解var等声明类语句的行为,而不理解指令序言,我们就无法解释严格模式。
|
||||
|
||||
## 预处理
|
||||
|
||||
JavaScript执行前,会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提前处理var、函数声明、class、const和let这些语句,以确定其中变量的意义。
|
||||
|
||||
因为一些历史包袱,这一部分内容非常复杂,首先我们看一下var声明。
|
||||
|
||||
### var声明
|
||||
|
||||
var声明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量。
|
||||
|
||||
我们还是从实例来进行学习。
|
||||
|
||||
```
|
||||
var a = 1;
|
||||
|
||||
function foo() {
|
||||
console.log(a);
|
||||
var a = 2;
|
||||
}
|
||||
|
||||
foo();
|
||||
|
||||
```
|
||||
|
||||
这段代码声明了一个脚本级别的a,又声明了foo函数体级别的a,我们注意到,函数体级的`var`出现在console.log语句之后。
|
||||
|
||||
但是预处理过程在执行之前,所以有函数体级的变量a,就不会去访问外层作用域中的变量a了,而函数体级的变量a此时还没有赋值,所以是undefined。我们再看一个情况:
|
||||
|
||||
```
|
||||
var a = 1;
|
||||
|
||||
function foo() {
|
||||
console.log(a);
|
||||
if(false) {
|
||||
var a = 2;
|
||||
}
|
||||
}
|
||||
|
||||
foo();
|
||||
|
||||
```
|
||||
|
||||
这段代码比上一段代码在`var a = 2`之外多了一段if,我们知道if(false)中的代码永远不会被执行,但是预处理阶段并不管这个,var的作用能够穿透一切语句结构,它只认脚本、模块和函数体三种语法结构。所以这里结果跟前一段代码完全一样,我们会得到undefined。
|
||||
|
||||
我们看下一个例子,我们在运行时部分讲过类似的例子。
|
||||
|
||||
```
|
||||
var a = 1;
|
||||
|
||||
function foo() {
|
||||
var o= {a:3}
|
||||
with(o) {
|
||||
var a = 2;
|
||||
}
|
||||
console.log(o.a);
|
||||
console.log(a);
|
||||
}
|
||||
|
||||
foo();
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,我们引入了with语句,我们用with(o)创建了一个作用域,并把o对象加入词法环境,在其中使用了`var a = 2;`语句。
|
||||
|
||||
在预处理阶段,只认`var`中声明的变量,所以同样为foo的作用域创建了a这个变量,但是没有赋值。
|
||||
|
||||
在执行阶段,当执行到`var a = 2`时,作用域变成了with语句内,这时候的a被认为访问到了对象o的属性a,所以最终执行的结果,我们得到了2和undefined。
|
||||
|
||||
这个行为是JavaScript公认的设计失误之一,一个语句中的a在预处理阶段和执行阶段被当做两个不同的变量,严重违背了直觉,但是今天,在JavaScript设计原则“don’t break the web”之下,已经无法修正了,所以你需要特别注意。
|
||||
|
||||
因为早年JavaScript没有let和const,只能用var,又因为var除了脚本和函数体都会穿透,人民群众发明了“立即执行的函数表达式(IIFE)”这一用法,用来产生作用域,例如:
|
||||
|
||||
```
|
||||
|
||||
for(var i = 0; i < 20; i ++) {
|
||||
void function(i){
|
||||
var div = document.createElement("div");
|
||||
div.innerHTML = i;
|
||||
div.onclick = function(){
|
||||
console.log(i);
|
||||
}
|
||||
document.body.appendChild(div);
|
||||
}(i);
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
这段代码非常经典,常常在实际开发中见到,也经常被用作面试题,为文档添加了20个div元素,并且绑定了点击事件,打印它们的序号。
|
||||
|
||||
我们通过IIFE在循环内构造了作用域,每次循环都产生一个新的环境记录,这样,每个div都能访问到环境中的i。
|
||||
|
||||
如果我们不用IIFE:
|
||||
|
||||
```
|
||||
for(var i = 0; i < 20; i ++) {
|
||||
var div = document.createElement("div");
|
||||
div.innerHTML = i;
|
||||
div.onclick = function(){
|
||||
console.log(i);
|
||||
}
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码的结果将会是点每个div都打印20,因为全局只有一个i,执行完循环后,i变成了20。
|
||||
|
||||
### function声明
|
||||
|
||||
function声明的行为原本跟var非常相似,但是在最新的JavaScript标准中,对它进行了一定的修改,这让情况变得更加复杂了。
|
||||
|
||||
在全局(脚本、模块和函数体),function声明表现跟var相似,不同之处在于,function声明不但在作用域中加入变量,还会给它赋值。
|
||||
|
||||
我们看一下function声明的例子:
|
||||
|
||||
```
|
||||
console.log(foo);
|
||||
function foo(){
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里声明了函数foo,在声明之前,我们用console.log打印函数foo,我们可以发现,已经是函数foo的值了。
|
||||
|
||||
function声明出现在if等语句中的情况有点复杂,它仍然作用于脚本、模块和函数体级别,在预处理阶段,仍然会产生变量,它不再被提前赋值:
|
||||
|
||||
```
|
||||
console.log(foo);
|
||||
if(true) {
|
||||
function foo(){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码得到undefined。如果没有函数声明,则会抛出错误。
|
||||
|
||||
这说明function在预处理阶段仍然发生了作用,在作用域中产生了变量,没有产生赋值,赋值行为发生在了执行阶段。
|
||||
|
||||
出现在if等语句中的function,在if创建的作用域中仍然会被提前,产生赋值效果,我们会在下一节课继续讨论。
|
||||
|
||||
### class声明
|
||||
|
||||
class声明在全局的行为跟function和var都不一样。
|
||||
|
||||
在class声明之前使用class名,会抛错:
|
||||
|
||||
```
|
||||
console.log(c);
|
||||
class c{
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码我们试图在class前打印变量c,我们得到了个错误,这个行为很像是class没有预处理,但是实际上并非如此。
|
||||
|
||||
我们看个复杂一点的例子:
|
||||
|
||||
```
|
||||
var c = 1;
|
||||
function foo(){
|
||||
console.log(c);
|
||||
class c {}
|
||||
}
|
||||
foo();
|
||||
|
||||
```
|
||||
|
||||
这个例子中,我们把class放进了一个函数体中,在外层作用域中有变量c。然后试图在class之前打印c。
|
||||
|
||||
执行后,我们看到,仍然抛出了错误,如果去掉class声明,则会正常打印出1,也就是说,出现在后面的class声明影响了前面语句的结果。
|
||||
|
||||
这说明,class声明也是会被预处理的,它会在作用域中创建变量,并且要求访问它时抛出错误。
|
||||
|
||||
class的声明作用不会穿透if等语句结构,所以只有写在全局环境才会有声明作用,这部分我们将会在下一节课讲解。
|
||||
|
||||
这样的class设计比function和var更符合直觉,而且在遇到一些比较奇怪的用法时,倾向于抛出错误。
|
||||
|
||||
按照现代语言设计的评价标准,及早抛错是好事,它能够帮助我们尽量在开发阶段就发现代码的可能问题。
|
||||
|
||||
## 指令序言机制
|
||||
|
||||
脚本和模块都支持一种特别的语法,叫做指令序言(Directive Prologs)。
|
||||
|
||||
这里的指令序言最早是为了use strict设计的,它规定了一种给JavaScript代码添加元信息的方式。
|
||||
|
||||
```
|
||||
"use strict";
|
||||
function f(){
|
||||
console.log(this);
|
||||
};
|
||||
f.call(null);
|
||||
|
||||
```
|
||||
|
||||
这段代码展示了严格模式的用法,我这里定义了函数f,f中打印this值,然后用call的方法调用f,传入null作为this值,我们可以看到最终结果是null原封不动地被当做this值打印了出来,这是严格模式的特征。
|
||||
|
||||
如果我们去掉严格模式的指令需要,打印的结果将会变成global。
|
||||
|
||||
`"use strict"`是JavaScript标准中规定的唯一一种指令序言,但是设计指令序言的目的是,留给JavaScript的引擎和实现者一些统一的表达方式,在静态扫描时指定JavaScript代码的一些特性。
|
||||
|
||||
例如,假设我们要设计一种声明本文件不需要进行lint检查的指令,我们可以这样设计:
|
||||
|
||||
```
|
||||
"no lint";
|
||||
"use strict";
|
||||
function doSth(){
|
||||
//......
|
||||
}
|
||||
//......
|
||||
|
||||
```
|
||||
|
||||
JavaScript的指令序言是只有一个字符串直接量的表达式语句,它只能出现在脚本、模块和函数体的最前面。
|
||||
|
||||
我们看两个例子:
|
||||
|
||||
```
|
||||
function doSth(){
|
||||
//......
|
||||
}
|
||||
"use strict";
|
||||
var a = 1;
|
||||
//......
|
||||
|
||||
```
|
||||
|
||||
这个例子中,`"use strict"`没有出现在最前,所以不是指令序言。
|
||||
|
||||
```
|
||||
'use strict';
|
||||
function doSth(){
|
||||
//......
|
||||
}
|
||||
var a = 1;
|
||||
//......
|
||||
|
||||
```
|
||||
|
||||
这个例子中,`'use strict'`是单引号,这不妨碍它仍然是指令序言。
|
||||
|
||||
## 结语
|
||||
|
||||
今天,我们一起进入了JavaScript的语法部分,在开始学习之前,我先介绍了一部分语法的基本规则。
|
||||
|
||||
我们首先介绍了JavaScript语法的全局结构,JavaScript有两种源文件,一种叫做脚本,一种叫做模块。介绍完脚本和模块的基础概念,我们再来把它们往下分,脚本中可以包含语句。模块中可以包含三种内容:import声明,export声明和语句。
|
||||
|
||||
最后,我介绍了两个JavaScript语法的全局机制:预处理和指令序言。
|
||||
|
||||
最后,给你留一个小任务,我们试着用babel,分析一段JavaScript的模块代码,并且找出它中间的所有export的变量。
|
||||
|
||||
|
||||
276
极客时间专栏/重学前端/模块一:JavaScript/JavaScript语法(三):什么是表达式语句?.md
Normal file
276
极客时间专栏/重学前端/模块一:JavaScript/JavaScript语法(三):什么是表达式语句?.md
Normal file
@@ -0,0 +1,276 @@
|
||||
<audio id="audio" title="JavaScript语法(三):什么是表达式语句?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/65/23/651c19b1bc1688c685cd577366fc8c23.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
不知道你有没有注意到,我们在语句部分,讲到了很多种语句类型,但是,其实最终产生执行效果的语句不多。
|
||||
|
||||
事实上,真正能干活的就只有表达式语句,其它语句的作用都是产生各种结构,来控制表达式语句执行,或者改变表达式语句的意义。
|
||||
|
||||
今天的课程,我们就深入到表达式语句中来学习一下。
|
||||
|
||||
## 什么是表达式语句
|
||||
|
||||
表达式语句实际上就是一个表达式,它是由运算符连接变量或者直接量构成的(关于直接量我们在下一节详细讲解)。
|
||||
|
||||
一般来说,我们的表达式语句要么是函数调用,要么是赋值,要么是自增、自减,否则表达式计算的结果没有任何意义。
|
||||
|
||||
但是从语法上,并没有这样的限制,任何合法的表达式都可以当做表达式语句使用。比如我们看下面的例子。
|
||||
|
||||
```
|
||||
a + b;
|
||||
|
||||
```
|
||||
|
||||
这句代码计算了a和b相加的值,但是不会显示出来,也不会产生任何执行效果(除非a和b是getter),但是不妨碍它符合语法也能够被执行。
|
||||
|
||||
下面我们就一起来了解下都有哪些表达式,我们从粒度最小到粒度最大了解一下。
|
||||
|
||||
## PrimaryExpression 主要表达式
|
||||
|
||||
首先我们来给你讲解一下表达式的原子项:Primary Expression。它是表达式的最小单位,它所涉及的语法结构也是优先级最高的。
|
||||
|
||||
Primary Expression包含了各种“直接量”,直接量就是直接用某种语法写出来的具有特定类型的值。我们已经知道,在运行时有各种值,比如数字123,字符串Hello world,所以通俗地讲,直接量就是在代码中把它们写出来的语法。
|
||||
|
||||
我们在类型部分,已经介绍过一些基本类型的直接量。比如,我们当时用null关键字获取null值,这个用法就是null直接量,这里我们仅仅把它们简单回顾一下:
|
||||
|
||||
```
|
||||
"abc";
|
||||
123;
|
||||
null;
|
||||
true;
|
||||
false;
|
||||
|
||||
```
|
||||
|
||||
除这些之外,JavaScript还能够直接量的形式定义对象,针对函数、类、数组、正则表达式等特殊对象类型,JavaScript提供了语法层面的支持。
|
||||
|
||||
```
|
||||
({});
|
||||
(function(){});
|
||||
(class{ });
|
||||
[];
|
||||
/abc/g;
|
||||
|
||||
```
|
||||
|
||||
需要注意,在语法层面,function、{ 和class开头的表达式语句与声明语句有语法冲突,所以,我们要想使用这样的表达式,必须加上括号来回避语法冲突。
|
||||
|
||||
在JavaScript标准中,这些结构有的被称作直接量(Literal),有的被称作表达式(**Expression),在我看来,把它们都理解成直接量比较合适。
|
||||
|
||||
Primary Expression还可以是this或者变量,在语法上,把变量称作“标识符引用”。
|
||||
|
||||
```
|
||||
this;
|
||||
myVar;
|
||||
|
||||
```
|
||||
|
||||
任何表达式加上圆括号,都被认为是Primary Expression,这个机制使得圆括号成为改变运算优先顺序的手段。
|
||||
|
||||
```
|
||||
(a + b);
|
||||
|
||||
```
|
||||
|
||||
这就是Primary Expression的几种形式了,接下来,我们讲讲由Primary Expression构成的更复杂的表达式:Member Expression。
|
||||
|
||||
## MemberExpression 成员表达式
|
||||
|
||||
Member Expression通常是用于访问对象成员的。它有几种形式:
|
||||
|
||||
```
|
||||
a.b;
|
||||
a["b"];
|
||||
new.target;
|
||||
super.b;
|
||||
|
||||
```
|
||||
|
||||
前面两种用法都很好理解,就是用标识符的属性访问和用字符串的属性访问。而new.target是个新加入的语法,用于判断函数是否是被new调用,super则是构造函数中,用于访问父类的属性的语法。
|
||||
|
||||
从名字就可以看出,Member Expression最初设计是为了属性访问的,不过从语法结构需要,以下两种在JavaScript标准中当做Member Expression:
|
||||
|
||||
```
|
||||
f`a${b}c`;
|
||||
|
||||
```
|
||||
|
||||
这是一个是带函数的模板,这个带函数名的模板表示把模板的各个部分算好后传递给一个函数。
|
||||
|
||||
```
|
||||
new Cls();
|
||||
|
||||
```
|
||||
|
||||
另一个是带参数列表的new运算,注意,不带参数列表的new运算优先级更低,不属于Member Expression。
|
||||
|
||||
实际上,这两种被放入Member Expression,仅仅意味着它们跟属性运算属于同一优先级,没有任何语义上的关联。接下来我们看看Member Expression能组成什么。
|
||||
|
||||
## NewExpression NEW表达式
|
||||
|
||||
这种非常简单,Member Expression加上new就是New Expression(当然,不加new也可以构成New Expression,JavaScript中默认独立的高优先级表达式都可以构成低优先级表达式)。
|
||||
|
||||
注意,这里的New Expression特指没有参数列表的表达式。我们看个稍微复杂的例子:
|
||||
|
||||
```
|
||||
new new Cls(1);
|
||||
|
||||
```
|
||||
|
||||
直观看上去,它可能有两种意思:
|
||||
|
||||
```
|
||||
new (new Cls(1));
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
new (new Cls)(1);
|
||||
|
||||
```
|
||||
|
||||
实际上,它等价于第一种。我们可以用以下代码来验证:
|
||||
|
||||
```
|
||||
class Cls{
|
||||
constructor(n){
|
||||
console.log("cls", n);
|
||||
return class {
|
||||
constructor(n) {
|
||||
console.log("returned", n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new (new Cls(1));
|
||||
|
||||
```
|
||||
|
||||
这段代码最后得到了下面这样的结果。
|
||||
|
||||
```
|
||||
cls 1
|
||||
returned undefined
|
||||
|
||||
```
|
||||
|
||||
这里就说明了,1被当做调用Cls时的参数传入了。
|
||||
|
||||
## CallExpression 函数调用表达式
|
||||
|
||||
除了New Expression,Member Expression还能构成Call Expression。它的基本形式是Member Expression后加一个括号里的参数列表,或者我们可以用上super关键字代替Member Expression。
|
||||
|
||||
```
|
||||
a.b(c);
|
||||
super();
|
||||
|
||||
```
|
||||
|
||||
这看起来很简单,但是它有一些变体。比如:
|
||||
|
||||
```
|
||||
a.b(c)(d)(e);
|
||||
a.b(c)[3];
|
||||
a.b(c).d;
|
||||
a.b(c)`xyz`;
|
||||
|
||||
```
|
||||
|
||||
这些变体的形态,跟Member Expression几乎是一一对应的。实际上,我们可以理解为,Member Expression中的某一子结构具有函数调用,那么整个表达式就成为了一个Call Expression。
|
||||
|
||||
而Call Expression就失去了比New Expression优先级高的特性,这是一个主要的区分。
|
||||
|
||||
## LeftHandSideExpression 左值表达式
|
||||
|
||||
接下来,我们需要理解一个概念:New Expression 和 Call Expression 统称LeftHandSideExpression,左值表达式。
|
||||
|
||||
我们直观地讲,左值表达式就是可以放到等号左边的表达式。JavaScript语法则是下面这样。
|
||||
|
||||
```
|
||||
a() = b;
|
||||
|
||||
```
|
||||
|
||||
这样的用法其实是符合语法的,只是,原生的JavaScript函数,返回的值都不能被赋值。因此多数时候,我们看到的赋值将会是Call Expression的其它形式,如:
|
||||
|
||||
```
|
||||
a().c = b;
|
||||
|
||||
```
|
||||
|
||||
另外,根据JavaScript运行时的设计,不排除某些宿主会提供返回引用类型的函数,这时候,赋值就是有效的了。
|
||||
|
||||
左值表达式最经典的用法是用于构成赋值表达式,但是其实如果你翻一翻JavaScript标准,你会发现它出现在各种场合,凡是需要“可以被修改的变量”的位置,都能见到它的身影。
|
||||
|
||||
那么接下来我们就讲讲 AssignmentExpression 赋值表达式。
|
||||
|
||||
## AssignmentExpression 赋值表达式
|
||||
|
||||
AssignmentExpression 赋值表达式也有多种形态,最基本的当然是使用等号赋值:
|
||||
|
||||
```
|
||||
a = b
|
||||
|
||||
```
|
||||
|
||||
这里需要理解的一个稍微复杂的概念是,这个等号是可以嵌套的:
|
||||
|
||||
```
|
||||
a = b = c = d
|
||||
|
||||
```
|
||||
|
||||
这样的连续赋值,是右结合的,它等价于下面这种:
|
||||
|
||||
```
|
||||
a = (b = (c = d))
|
||||
|
||||
```
|
||||
|
||||
也就是说,先把d的结果赋值给c,再把整个表达式的结果赋值给b,再赋值给a。
|
||||
|
||||
**当然,这并非一个很好的代码风格,我们讲解语法是为了让你理解这样的用法,而不是推荐你这样写代码。**
|
||||
|
||||
赋值表达式的使用,还可以结合一些运算符,例如:
|
||||
|
||||
```
|
||||
a += b;
|
||||
|
||||
```
|
||||
|
||||
相当于
|
||||
|
||||
```
|
||||
a = a + b;
|
||||
|
||||
```
|
||||
|
||||
能有这样用的运算符有下面这几种:
|
||||
|
||||
`*=`、`/=`、`%=`、`+=`、`-=`、`<<=`、`>>=`、`>>>=`、`&=`、`^=`、`|=`、`**=`
|
||||
|
||||
我想你已经注意到了,赋值表达式的等号左边和右边能用的表达式类型不一样,在这一课,我们已经关注完了表达式的左边部分(左值表达式)的语法结构,下一节课,我们将会给你重点讲解表达式的右边部分。
|
||||
|
||||
## Expression 表达式
|
||||
|
||||
赋值表达式可以构成Expression表达式的一部分。在JavaScript中,表达式就是用逗号运算符连接的赋值表达式。
|
||||
|
||||
在JavaScript中,比赋值运算优先级更低的就是逗号运算符了。我们可以把逗号可以理解为一种小型的分号。
|
||||
|
||||
```
|
||||
a = b, b = 1, null;
|
||||
|
||||
```
|
||||
|
||||
逗号分隔的表达式会顺次执行,就像不同的表达式语句一样。“整个表达式的结果”就是“最后一个逗号后的表达式结果”。比如我们文中的例子,整个`“a = b, b = 1, null;”`表达式的结果就是`“,”`后面的`null`。
|
||||
|
||||
在很多场合,都不允许使用带逗号的表达式,比如我们在前面课程中提到,export后只能跟赋值表达式,意思就是表达式中不能含有逗号。
|
||||
|
||||
## 结语
|
||||
|
||||
这节课我们开始讲解了运算符和表达式的一些相关知识,这节课上,我们已经学习了赋值表达式和赋值表达式的左边部分。下节课,我们将会讲一讲赋值表达式的右边部分。
|
||||
|
||||
最后给你留一个作业,把今天讲到的所有运算符按优先级排列成一个表格,下节课我们会补完剩下的部分。
|
||||
|
||||
|
||||
545
极客时间专栏/重学前端/模块一:JavaScript/JavaScript语法(二):你知道哪些JavaScript语句?.md
Normal file
545
极客时间专栏/重学前端/模块一:JavaScript/JavaScript语法(二):你知道哪些JavaScript语句?.md
Normal file
@@ -0,0 +1,545 @@
|
||||
<audio id="audio" title="JavaScript语法(二):你知道哪些JavaScript语句?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b9/36/b98805c10ae51f49492bce8a7103b936.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
我们在上一节课中已经讲过了JavaScript语法的顶层设计,接下来我们进入到更具体的内容。
|
||||
|
||||
JavaScript遵循了一般编程语言的“语句-表达式”结构,多数编程语言都是这样设计的。我们在上节课讲的脚本,或者模块都是由语句列表构成的,这一节课,我们就来一起了解一下语句。
|
||||
|
||||
在JavaScript标准中,把语句分成了两种:声明和语句,不过,这里的区分逻辑比较奇怪,所以,这里我还是按照自己的思路给你整理一下。
|
||||
|
||||
普通语句:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/55/8186219674547691cf59e5c095304d55.png" alt="">
|
||||
|
||||
声明型语句:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/38/0e5327528df12d1eaad52c4005efff38.jpg" alt="">
|
||||
|
||||
我们根据上面的分类,来遍历学习一下这些语句。
|
||||
|
||||
## 语句块
|
||||
|
||||
我们可以这样去简单理解,语句块就是一对大括号。
|
||||
|
||||
```
|
||||
{
|
||||
var x, y;
|
||||
x = 10;
|
||||
y = 20;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
语句块的意义和好处在于:让我们可以把多行语句视为同一行语句,这样,if、for等语句定义起来就比较简单了。不过,我们需要注意的是,语句块会产生作用域,我们看一个例子:
|
||||
|
||||
```
|
||||
{
|
||||
let x = 1;
|
||||
}
|
||||
console.log(x); // 报错
|
||||
|
||||
```
|
||||
|
||||
这里我们的let声明,仅仅对语句块作用域生效,于是我们在语句块外试图访问语句块内的变量x就会报错。
|
||||
|
||||
## 空语句
|
||||
|
||||
空语句就是一个独立的分号,实际上没什么大用。我们来看一下:
|
||||
|
||||
```
|
||||
;
|
||||
|
||||
```
|
||||
|
||||
空语句的存在仅仅是从语言设计完备性的角度考虑,允许插入多个分号而不抛出错误。
|
||||
|
||||
## if语句
|
||||
|
||||
if语句是条件语句。我想,对多数人来说,if语句都是熟悉的老朋友了,也没有什么特别需要注意的用法,但是为了我们课程的完备性,这里还是要讲一下。
|
||||
|
||||
if语句示例如下:
|
||||
|
||||
```
|
||||
if(a < b)
|
||||
console.log(a);
|
||||
|
||||
```
|
||||
|
||||
if语句的作用是,在满足条件时执行它的内容语句,这个语句可以是一个语句块,这样就可以实现有条件地执行多个语句了。
|
||||
|
||||
if语句还有else结构,用于不满足条件时执行,一种常见的用法是,利用语句的嵌套能力,把if和else连写成多分支条件判断:
|
||||
|
||||
```
|
||||
if(a < 10) {
|
||||
//...
|
||||
} else if(a < 20) {
|
||||
//...
|
||||
} else if(a < 30) {
|
||||
//...
|
||||
} else {
|
||||
//...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码表示四个互斥的分支,分别在满足a<10、a<20、a<30和其它情况时执行。
|
||||
|
||||
## switch语句
|
||||
|
||||
switch语句继承自Java,Java中的switch语句继承自C和C++,原本switch语句是跳转的变形,所以我们如果要用它来实现分支,必须要加上break。
|
||||
|
||||
其实switch原本的设计是类似goto的思维。我们看一个例子:
|
||||
|
||||
```
|
||||
switch(num) {
|
||||
case 1:
|
||||
print(1);
|
||||
case 2:
|
||||
print 2;
|
||||
case 3:
|
||||
print 3;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码当num为1时输出1 2 3,当num为2时输出2 3,当num为3时输出3。如果我们要把它变成分支型,则需要在每个case后加上break。
|
||||
|
||||
```
|
||||
switch(num) {
|
||||
case 1:
|
||||
print 1;
|
||||
break;
|
||||
case 2:
|
||||
print 2;
|
||||
break;
|
||||
case 3:
|
||||
print 3;
|
||||
break;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在C时代,switch生成的汇编代码性能是略优于if else的,但是对JavaScript来说,则无本质区别。我个人的看法是,现在switch已经完全没有必要使用了,应该用if else结构代替。
|
||||
|
||||
## 循环语句
|
||||
|
||||
循环语句应该也是你所熟悉的语句了,这里我们把重点放在一些新用法上。
|
||||
|
||||
### while循环和do while循环
|
||||
|
||||
这两个都是历史悠久的JavaScript语法了,示例大概如下:
|
||||
|
||||
```
|
||||
let a = 100
|
||||
while(a--) {
|
||||
console.log("*");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
let a = 101;
|
||||
do {
|
||||
console.log(a);
|
||||
} while(a < 100)
|
||||
|
||||
```
|
||||
|
||||
注意,这里do while循环无论如何至少会执行一次。
|
||||
|
||||
### 普通for循环
|
||||
|
||||
首先我们来看看普通的for循环。
|
||||
|
||||
```
|
||||
|
||||
for(i = 0; i < 100; i++)
|
||||
console.log(i);
|
||||
|
||||
for(var i = 0; i < 100; i++)
|
||||
console.log(i);
|
||||
|
||||
for(let i = 0; i < 100; i++)
|
||||
console.log(i);
|
||||
|
||||
var j = 0;
|
||||
for(const i = 0; j < 100; j++)
|
||||
console.log(i);
|
||||
|
||||
|
||||
```
|
||||
|
||||
这里为了配合新语法,加入了允许let和const,实际上,const在这里是非常奇葩的东西,因为这里声明和初始化的变量,按惯例是用于控制循环的,但是它如果是const就没法改了。
|
||||
|
||||
我想,这一点可能是从保持let和const一致性的角度考虑的吧。
|
||||
|
||||
### for in循环
|
||||
|
||||
for in 循环枚举对象的属性,这里体现了属性的enumerable特征。
|
||||
|
||||
```
|
||||
let o = { a: 10, b: 20}
|
||||
Object.defineProperty(o, "c", {enumerable:false, value:30})
|
||||
|
||||
for(let p in o)
|
||||
console.log(p);
|
||||
|
||||
|
||||
```
|
||||
|
||||
这段代码中,我们定义了一个对象o,给它添加了不可枚举的属性c,之后我们用for in循环枚举它的属性,我们会发现,输出时得到的只有a和b。
|
||||
|
||||
如果我们定义c这个属性时,enumerable为true,则for in循环中也能枚举到它。
|
||||
|
||||
### for of循环和for await of循环
|
||||
|
||||
for of循环是非常棒的语法特性。
|
||||
|
||||
我们先看下基本用法,它可以用于数组:
|
||||
|
||||
```
|
||||
for(let e of [1, 2, 3, 4, 5])
|
||||
console.log(e);
|
||||
|
||||
```
|
||||
|
||||
但是实际上,它背后的机制是iterator机制。
|
||||
|
||||
我们可以给任何一个对象添加iterator,使它可以用于for of语句,看下示例:
|
||||
|
||||
```
|
||||
let o = {
|
||||
[Symbol.iterator]:() => ({
|
||||
_value: 0,
|
||||
next(){
|
||||
if(this._value == 10)
|
||||
return {
|
||||
done: true
|
||||
}
|
||||
else return {
|
||||
value: this._value++,
|
||||
done: false
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
for(let e of o)
|
||||
console.log(e);
|
||||
|
||||
|
||||
```
|
||||
|
||||
这段代码展示了如何为一个对象添加iterator。但是,在实际操作中,我们一般不需要这样定义iterator,我们可以使用generator function。
|
||||
|
||||
```
|
||||
function* foo(){
|
||||
yield 0;
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}
|
||||
for(let e of foo())
|
||||
console.log(e);
|
||||
|
||||
```
|
||||
|
||||
这段代码展示了generator function和foo的配合。
|
||||
|
||||
此外,JavaScript还为异步生成器函数配备了异步的for of,我们来看一个例子:
|
||||
|
||||
```
|
||||
function sleep(duration) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve,duration);
|
||||
})
|
||||
}
|
||||
async function* foo(){
|
||||
i = 0;
|
||||
while(true) {
|
||||
await sleep(1000);
|
||||
yield i++;
|
||||
}
|
||||
|
||||
}
|
||||
for await(let e of foo())
|
||||
console.log(e);
|
||||
|
||||
```
|
||||
|
||||
这段代码定义了一个异步生成器函数,异步生成器函数每隔一秒生成一个数字,这是一个无限的生成器。
|
||||
|
||||
接下来,我们使用for await of来访问这个异步生成器函数的结果,我们可以看到,这形成了一个每隔一秒打印一个数字的无限循环。
|
||||
|
||||
但是因为我们这个循环是异步的,并且有时间延迟,所以,这个无限循环的代码可以用于显示时钟等有意义的操作。
|
||||
|
||||
## return
|
||||
|
||||
return语句用于函数中,它终止函数的执行,并且指定函数的返回值,这是大家非常熟悉语句了,也没有什么特殊之处。
|
||||
|
||||
```
|
||||
function squre(x){
|
||||
return x * x;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码展示了return的基本用法。它后面可以跟一个表达式,计算结果就是函数返回值。
|
||||
|
||||
## break语句和continue语句
|
||||
|
||||
break语句用于跳出循环语句或者switch语句,continue语句用于结束本次循环并继续循环。
|
||||
|
||||
这两个语句都属于控制型语句,用法也比较相似,所以我们就一起讲了。需要注意的是,它们都有带标签的用法。
|
||||
|
||||
```
|
||||
outer:for(let i = 0; i < 100; i++)
|
||||
inner:for(let j = 0; j < 100; j++)
|
||||
if( i == 50 && j == 50)
|
||||
break outer;
|
||||
outer:for(let i = 0; i < 100; i++)
|
||||
inner:for(let j = 0; j < 100; j++)
|
||||
if( i >= 50 && j == 50)
|
||||
continue outer;
|
||||
|
||||
```
|
||||
|
||||
带标签的break和continue可以控制自己被外层的哪个语句结构消费,这可以跳出复杂的语句结构。
|
||||
|
||||
## with语句
|
||||
|
||||
with语句是个非常巧妙的设计,但它把JavaScript的变量引用关系变得不可分析,所以一般都认为这种语句都属于糟粕。
|
||||
|
||||
但是历史无法改写,现在已经无法去除with了。我们来了解一下它的基本用法即可。
|
||||
|
||||
```
|
||||
let o = {a:1, b:2}
|
||||
with(o){
|
||||
console.log(a, b);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
with语句把对象的属性在它内部的作用域内变成变量。
|
||||
|
||||
## try语句和throw语句
|
||||
|
||||
try语句和throw语句用于处理异常。它们是配合使用的,所以我们就放在一起讲了。在大型应用中,异常机制非常重要。
|
||||
|
||||
```
|
||||
try {
|
||||
throw new Error("error");
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
console.log("finally");
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
一般来说,throw用于抛出异常,但是单纯从语言的角度,我们可以抛出任何值,也不一定是异常逻辑,但是为了保证语义清晰,不建议用throw表达任何非异常逻辑。
|
||||
|
||||
try语句用于捕获异常,用throw抛出的异常,可以在try语句的结构中被处理掉:try部分用于标识捕获异常的代码段,catch部分则用于捕获异常后做一些处理,而finally则是用于执行后做一些必须执行的清理工作。
|
||||
|
||||
catch结构会创建一个局部的作用域,并且把一个变量写入其中,需要注意,在这个作用域,不能再声明变量e了,否则会出错。
|
||||
|
||||
在catch中重新抛出错误的情况非常常见,在设计比较底层的函数时,常常会这样做,保证抛出的错误能被理解。
|
||||
|
||||
finally语句一般用于释放资源,它一定会被执行,我们在前面的课程中已经讨论过一些finally的特征,即使在try中出现了return,finally中的语句也一定要被执行。
|
||||
|
||||
## debugger语句
|
||||
|
||||
debugger语句的作用是:通知调试器在此断点。在没有调试器挂载时,它不产生任何效果。
|
||||
|
||||
介绍完普通语句,我们再来看看声明型语句。声明型语句跟普通语句最大区别就是声明型语句响应预处理过程,普通语句只有执行过程。
|
||||
|
||||
## var
|
||||
|
||||
var声明语句是古典的JavaScript中声明变量的方式。而现在,在绝大多数情况下,let和const都是更好的选择。
|
||||
|
||||
我们在上一节课已经讲解了var声明对全局作用域的影响,它是一种预处理机制。
|
||||
|
||||
如果我们仍然想要使用var,我的个人建议是,把它当做一种“保障变量是局部”的逻辑,遵循以下三条规则:
|
||||
|
||||
- 声明同时必定初始化;
|
||||
- 尽可能在离使用的位置近处声明;
|
||||
- 不要在意重复声明。
|
||||
|
||||
例如:
|
||||
|
||||
```
|
||||
var x = 1, y = 2;
|
||||
doSth(x, y);
|
||||
|
||||
for(var x = 0; x < 10; x++)
|
||||
doSth2(x);
|
||||
|
||||
```
|
||||
|
||||
这个例子中,两次声明了变量x,完成了两段逻辑,这两个x意义上可能不一定相关,这样,不论我们把代码复制粘贴在哪里,都不会出错。
|
||||
|
||||
当然,更好的办法是使用let改造,我们看看如何改造:
|
||||
|
||||
```
|
||||
{
|
||||
let x = 1, y = 2;
|
||||
doSth(x, y);
|
||||
}
|
||||
|
||||
for(let x = 0; x < 10; x++)
|
||||
doSth2(x);
|
||||
|
||||
```
|
||||
|
||||
这里我用代码块限制了第一个x的作用域,这样就更难发生变量命名冲突引起的错误了。
|
||||
|
||||
## let和const
|
||||
|
||||
let和const是都是变量的声明,它们的特性非常相似,所以我们放在一起讲了。let和const是新设计的语法,所以没有什么硬伤,非常地符合直觉。let和const的作用范围是if、for等结构型语句。
|
||||
|
||||
我们看下基本用法:
|
||||
|
||||
```
|
||||
const a = 2;
|
||||
if(true){
|
||||
const a = 1;
|
||||
console.log(a);
|
||||
}
|
||||
console.log(a);
|
||||
|
||||
```
|
||||
|
||||
这里的代码先在全局声明了变量a,接下来又在if内声明了a,if内构成了一个独立的作用域。
|
||||
|
||||
const和let语句在重复声明时会抛错,这能够有效地避免变量名无意中冲突:
|
||||
|
||||
```
|
||||
let a = 2
|
||||
const a = 1;
|
||||
|
||||
```
|
||||
|
||||
这段代码中,先用let声明了a,接下来又试图使用const声明变量a,这时,就会产生错误。
|
||||
|
||||
let和const声明虽然看上去是执行到了才会生效,但是实际上,它们还是会被预处理。如果当前作用域内有声明,就无法访问到外部的变量。我们来看这段代码:
|
||||
|
||||
```
|
||||
const a = 2;
|
||||
if(true){
|
||||
console.log(a); //抛错
|
||||
const a = 1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里在if的作用域中,变量a声明执行到之前,我们访问了变量a,这时会抛出一个错误,这说明const声明仍然是有预处理机制的。
|
||||
|
||||
在执行到const语句前,我们的JavaScript引擎就已经知道后面的代码将会声明变量a,从而不允许我们访问外层作用域中的a。
|
||||
|
||||
## class声明
|
||||
|
||||
我们在之前的课程中,已经了解过class相关的用法。这里我们再从语法的角度来看一遍:
|
||||
|
||||
```
|
||||
class a {
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
class最基本的用法只需要class关键字、名称和一对大括号。它的声明特征跟const和let类似,都是作用于块级作用域,预处理阶段则会屏蔽外部变量。
|
||||
|
||||
```
|
||||
const a = 2;
|
||||
if(true){
|
||||
console.log(a); //抛错
|
||||
class a {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
class内部,可以使用constructor关键字来定义构造函数。还能定义getter/setter和方法。
|
||||
|
||||
```
|
||||
class Rectangle {
|
||||
constructor(height, width) {
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
}
|
||||
// Getter
|
||||
get area() {
|
||||
return this.calcArea();
|
||||
}
|
||||
// Method
|
||||
calcArea() {
|
||||
return this.height * this.width;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个例子来自MDN,它展示了构造函数、getter和方法的定义。
|
||||
|
||||
以目前的兼容性,class中的属性只能写在构造函数中,相关标准正在TC39讨论。
|
||||
|
||||
需要注意,class默认内部的函数定义都是strict模式的。
|
||||
|
||||
## 函数声明
|
||||
|
||||
函数声明使用 function 关键字。
|
||||
|
||||
在上一节课中,我们已经讨论过函数声明对全局作用域的影响了。这一节课,我们来看看函数声明具体的内容,我们先看一下函数声明的几种类型。
|
||||
|
||||
```
|
||||
|
||||
function foo(){
|
||||
|
||||
}
|
||||
|
||||
function* foo(){
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}
|
||||
|
||||
async function foo(){
|
||||
await sleep(3000);
|
||||
|
||||
}
|
||||
|
||||
async function* foo(){
|
||||
await sleep(3000);
|
||||
yield 1;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
带*的函数是generator,我们在前面的部分已经见过它了。生成器函数可以理解为返回一个序列的函数,它的底层是iterator机制。
|
||||
|
||||
async函数是可以暂停执行,等待异步操作的函数,它的底层是Promise机制。异步生成器函数则是二者的结合。
|
||||
|
||||
函数的参数,可以只写形参名,现在还可以写默认参数和指定多个参数,看下例子:
|
||||
|
||||
```
|
||||
|
||||
function foo(a = 1, ...other) {
|
||||
console.log(a, other)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个形式可以代替一些对参数的处理代码,表意会更加清楚。
|
||||
|
||||
## 结语
|
||||
|
||||
今天我们一起学习了语句家族,语句分成了普通语句和声明型语句。
|
||||
|
||||
普通语句部分,建议你把重点放在循环语句上面。声明型语句我觉得都很重要,尤其是它们的行为。熟练掌握了它们,我们就可以在工作中去综合运用它们,从而减少代码中的错误。新特性大多可以帮助我们发现代码中的错误。
|
||||
|
||||
最后留一个小作业,请你找出所有具有Symbol.iterator的原生对象,并且看看它们的for of遍历行为。
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
<audio id="audio" title="JavaScript语法(四):新加入的**运算符,哪里有些不一样呢?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/af/4e/afb1ea6ce8222b91e446a4fd72d4da4e.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
上一节课我们已经给你介绍了表达式的一些结构,其中关于赋值表达式,我们讲完了它的左边部分,而留下了它右边部分,那么,我们这节课一起来详细讲解。
|
||||
|
||||
在一些通用的计算机语言设计理论中,能够出现在赋值表达式右边的叫做:右值表达式(RightHandSideExpression),而在JavaScript标准中,规定了在等号右边表达式叫做条件表达式(ConditionalExpression),不过,在JavaScript标准中,从未出现过右值表达式字样。
|
||||
|
||||
JavaScript标准也规定了左值表达式同时都是条件表达式(也就是右值表达式),此外,左值表达式也可以通过跟一定的运算符组合,逐级构成更复杂的结构,直到成为右值表达式。
|
||||
|
||||
关于这块的知识,我们有时会看到按照运算符来组织的讲解形式。
|
||||
|
||||
这样讲解形式是因为:对运算符来说的“优先级”,如果从我们语法的角度来看,那就是“表达式的结构”。讲“乘法运算的优先级高于加法”,从语法的角度看就是“乘法表达式和加号运算符构成加法表达式”。
|
||||
|
||||
对于右值表达式来说,我们可以理解为以左值表达式为最小单位开始构成的,接下来我们就来看看左值表达式是如何一步步构成更为复杂的语法结构。
|
||||
|
||||
## 更新表达式 UpdateExpression
|
||||
|
||||
左值表达式搭配 `++` `--` 运算符,可以形成更新表达式。
|
||||
|
||||
```
|
||||
-- a;
|
||||
++ a;
|
||||
a --
|
||||
a ++
|
||||
|
||||
```
|
||||
|
||||
更新表达式会改变一个左值表达式的值。分为前后自增,前后自减一共四种。
|
||||
|
||||
我们要注意一下,这里在ES2018中,跟早期版本有所不同,前后自增自减运算被放到了同一优先级。
|
||||
|
||||
## 一元运算表达式 UnaryExpression
|
||||
|
||||
更新表达式搭配一元运算符,可以形成一元运算表达式,我们看下例子:
|
||||
|
||||
```
|
||||
delete a.b;
|
||||
void a;
|
||||
typeof a;
|
||||
- a;
|
||||
~ a;
|
||||
! a;
|
||||
await a;
|
||||
|
||||
```
|
||||
|
||||
它的特点就是一个更新表达式搭配了一个一元运算符。
|
||||
|
||||
## 乘方表达式 ExponentiationExpression
|
||||
|
||||
乘方表达式也是由更新表达式构成的。它使用`**`号。
|
||||
|
||||
```
|
||||
++i ** 30
|
||||
2 ** 30 //正确
|
||||
-2 ** 30 //报错
|
||||
|
||||
```
|
||||
|
||||
我们看一下例子,-2这样的一元运算表达式,是不可以放入乘方表达式的,如果需要表达类似的逻辑,必须加括号。
|
||||
|
||||
这里我们需要注意一下结合性,**运算是右结合的,这跟其它正常的运算符(也就是左结合运算符)都不一样。
|
||||
|
||||
我们来看一个例子。
|
||||
|
||||
```
|
||||
4 ** 3 ** 2
|
||||
|
||||
```
|
||||
|
||||
事实上,它是这样被运算的:
|
||||
|
||||
```
|
||||
4 ** (3 ** 2)
|
||||
|
||||
```
|
||||
|
||||
而不是这样被运算的:
|
||||
|
||||
```
|
||||
(4 ** 3) ** 2
|
||||
|
||||
```
|
||||
|
||||
我们来实际在代码中执行一下试试。最终结果是262144, 而不是4096。
|
||||
|
||||
## 乘法表达式 MultiplicativeExpression
|
||||
|
||||
到这里,我们进入了比较熟悉的表达式类型,乘方表达式可以构成乘法表达式,用乘号或者除号、取余符号连接就可以了,我们看看例子:
|
||||
|
||||
```
|
||||
x * 2;
|
||||
|
||||
```
|
||||
|
||||
乘法表达式有三种运算符:
|
||||
|
||||
```
|
||||
*
|
||||
/
|
||||
%
|
||||
|
||||
```
|
||||
|
||||
它们分别表示乘、除和取余。它们的优先级是一样的,所以统一放在乘法运算表达式中。
|
||||
|
||||
## 加法表达式 AdditiveExpression
|
||||
|
||||
加法表达式是由乘法表达式用加号或者减号连接构成的。我们看下例子:
|
||||
|
||||
```
|
||||
a + b * c
|
||||
|
||||
```
|
||||
|
||||
加法表达式有加号和减号两种运算符。
|
||||
|
||||
```
|
||||
+
|
||||
-
|
||||
|
||||
```
|
||||
|
||||
这就是我们小学学的加法和减法的意思了。不过要注意,加号还能表示字符串连接,这也比较符合一般的直觉。
|
||||
|
||||
## 移位表达式 ShiftExpression
|
||||
|
||||
移位表达式由加法表达式构成,移位是一种位运算,分成三种:
|
||||
|
||||
```
|
||||
<< 向左移位
|
||||
>> 向右移位
|
||||
>>> 无符号向右移位
|
||||
|
||||
```
|
||||
|
||||
移位运算把操作数看做二进制表示的整数,然后移动特定位数。所以左移n位相当于乘以2的n次方,右移n位相当于除以2取整n次。
|
||||
|
||||
普通移位会保持正负数。无符号移位会把减号视为符号位1,同时参与移位:
|
||||
|
||||
```
|
||||
-1 >>> 1
|
||||
|
||||
```
|
||||
|
||||
这个会得到2147483647,也就是2的31次方,跟负数的二进制表示法相关,这里就不详细讲解了。
|
||||
|
||||
在JavaScript中,二进制操作整数并不能提高性能,移位运算这里也仅仅作为一种数学运算存在,这些运算存在的意义也仅仅是照顾C系语言用户的习惯了。
|
||||
|
||||
## 关系表达式 RelationalExpression
|
||||
|
||||
移位表达式可以构成关系表达式,这里的关系表达式就是大于、小于、大于等于、小于等于等运算符号连接,统称为关系运算。
|
||||
|
||||
```
|
||||
<=
|
||||
>=
|
||||
<
|
||||
>
|
||||
instanceof
|
||||
in
|
||||
|
||||
```
|
||||
|
||||
需要注意,这里的<= 和 >= 关系运算,完全是针对数字的,所以 <= 并不等价于 < 或 ==。例如:
|
||||
|
||||
```
|
||||
null <= undefined
|
||||
//false
|
||||
null == undefined
|
||||
//true
|
||||
|
||||
```
|
||||
|
||||
请你务必不要用数学上的定义去理解这些运算符。
|
||||
|
||||
## 相等表达式 EqualityExpression
|
||||
|
||||
在语法上,相等表达式是由关系表达式用相等比较运算符(如 `==`)连接构成的。所以我们可以像下面这段代码一样使用,而不需要加括号。
|
||||
|
||||
```
|
||||
a instanceof "object" == true
|
||||
|
||||
```
|
||||
|
||||
相等表达式由四种运算符和关系表达式构成,我们来看一下运算符:
|
||||
|
||||
- `==`
|
||||
- `!=`
|
||||
- `===`
|
||||
- `!==`
|
||||
|
||||
相等表达式又包含一个JavaScript中著名的设计失误,那就是 `==` 的行为。
|
||||
|
||||
一些编程规范甚至要求完全避免使用 `==` 运算,我觉得这样规定是比较合理的,但是这里我还是尽量解释一下 `==` 的行为。
|
||||
|
||||
虽然标准中写的`==`十分复杂,但是归根结底,类型不同的变量比较时`==`运算只有三条规则:
|
||||
|
||||
- undefined与null相等;
|
||||
- 字符串和bool都转为数字再比较;
|
||||
- 对象转换成primitive类型再比较。
|
||||
|
||||
这样我们就可以理解一些不太符合直觉的例子了,比如:
|
||||
|
||||
- `false == '0'` true
|
||||
- `true == 'true'` false
|
||||
- `[] == 0` true
|
||||
- `[] == false` true
|
||||
- `new Boolean('false') == false` false
|
||||
|
||||
这里不太符合直觉的有两点:
|
||||
|
||||
- 一个是即使字符串与boolean比较,也都要转换成数字;
|
||||
- 另一个是对象如果转换成了primitive类型跟等号另一边类型恰好相同,则不需要转换成数字。
|
||||
|
||||
此外,`==` 的行为也经常跟if的行为(转换为boolean)混淆。总之,我建议,仅在确认 `==` 发生在Number和String类型之间时使用,比如:
|
||||
|
||||
```
|
||||
document.getElementsByTagName('input')[0].value == 100
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,等号左边必然是string,右边的直接量必然是number,这样使用 == 就没有问题了。
|
||||
|
||||
## 位运算表达式
|
||||
|
||||
位运算表达式含有三种:
|
||||
|
||||
- 按位与表达式 BitwiseANDExpression
|
||||
- 按位异或表达式 BitwiseANDExpression
|
||||
- 按位或表达式 BitwiseORExpression。
|
||||
|
||||
位运算表达式关系比较紧密,我们这里放到一起来讲。
|
||||
|
||||
按位与表达式由按位与运算符(`&`)连接按位异或表达式构成,按位与表达式把操作数视为二进制整数,然后把两个操作数按位做与运算。
|
||||
|
||||
按位异或表达式由按位异或运算符(`^`)连接按位与表达式构成,按位异或表达式把操作数视为二进制整数,然后把两个操作数按位做异或运算。异或两位相同时得0,两位不同时得1。
|
||||
|
||||
异或运算有个特征,那就是两次异或运算相当于取消。所以有一个异或运算的小技巧,就是用异或运算来交换两个整数的值。
|
||||
|
||||
```
|
||||
|
||||
let a = 102, b = 324;
|
||||
|
||||
a = a ^ b;
|
||||
b = a ^ b;
|
||||
a = a ^ b;
|
||||
|
||||
console.log(a, b);
|
||||
|
||||
```
|
||||
|
||||
按位或表达式由按位或运算符(`|`)连接相等表达式构成,按位或表达式把操作数视为二进制整数,然后把两个操作数按位做或运算。
|
||||
|
||||
按位或运算常常被用在一种叫做Bitmask的技术上。Bitmask相当于使用一个整数来当做多个布尔型变量,现在已经不太提倡了。不过一些比较老的API还是会这样设计,比如我们在DOM课程中,提到过的Iterator API,我们看下例子:
|
||||
|
||||
```
|
||||
var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, null, false);
|
||||
var node;
|
||||
while(node = iterator.nextNode())
|
||||
{
|
||||
console.log(node);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里的第二个参数就是使用了Bitmask技术,所以必须配合位运算表达式才能方便地传参。
|
||||
|
||||
## 逻辑与表达式和逻辑或表达式
|
||||
|
||||
逻辑与表达式由按位或表达式经过逻辑与运算符连接构成,逻辑或表达式则由逻辑与表达式经逻辑或运算符连接构成。
|
||||
|
||||
这里需要注意的是,这两种表达式都不会做类型转换,所以尽管是逻辑运算,但是最终的结果可能是其它类型。
|
||||
|
||||
比如:
|
||||
|
||||
```
|
||||
false || 1;
|
||||
|
||||
```
|
||||
|
||||
这句将会得到结果 1。
|
||||
|
||||
```
|
||||
false && undefined;
|
||||
|
||||
```
|
||||
|
||||
这句将会得到undefined。
|
||||
|
||||
另外还有一点,就是逻辑表达式具有短路的特性,例如:
|
||||
|
||||
```
|
||||
true || foo();
|
||||
|
||||
```
|
||||
|
||||
这里的foo将不会被执行,这种中断后面表达式执行的特性就叫做短路。
|
||||
|
||||
## 条件表达式 ConditionalExpression
|
||||
|
||||
条件表达式由逻辑或表达式和条件运算符构成,条件运算符又称三目运算符,它有三个部分,由两个运算符`?`和`:`配合使用。
|
||||
|
||||
```
|
||||
condition ? branch1 : branch2
|
||||
|
||||
```
|
||||
|
||||
这里需要注意,条件表达式也像逻辑表达式一样,可能忽略后面表达式的计算。这一点跟C语言的条件表达式是不一样的。
|
||||
|
||||
条件表达式实际上就是JavaScript中的右值表达式了 RightHandSideExpression,是可以放到赋值运算后面的表达式。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲解了表达式的右边部分,讲到了包括更新表达式、一元运算表达式、乘方表达式、乘法表达式、移位表达式等14种表达式。至此为止,我们已经讲全了表达式。你如果有不熟悉的地方,可以随时回头查阅。
|
||||
|
||||
留一个小任务,我们试着总结下JavaScript中所有的运算符优先级和结合性。例如:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/ca/4cb75eb863d5dffe7e9b6b0fb1161aca.jpg" alt="">
|
||||
|
||||
|
||||
245
极客时间专栏/重学前端/模块一:JavaScript/JavaScript语法(预备篇):到底要不要写分号呢?.md
Normal file
245
极客时间专栏/重学前端/模块一:JavaScript/JavaScript语法(预备篇):到底要不要写分号呢?.md
Normal file
@@ -0,0 +1,245 @@
|
||||
<audio id="audio" title="JavaScript语法(预备篇):到底要不要写分号呢?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ac/d1/accffbf5ccf1d32f20fdcba676b784d1.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
在我们介绍JavaScript语法的全局结构之前,我们先要探讨一个语言风格问题:究竟要不要写分号。
|
||||
|
||||
这是一个非常经典的口水问题,“加分号”党和“不写分号”党之间的战争,可谓是经久不息。
|
||||
|
||||
实际上,行尾使用分号的风格来自于Java,也来自于C语言和C++,这一设计最初是为了降低编译器的工作负担。
|
||||
|
||||
但是,从今天的角度来看,行尾使用分号其实是一种语法噪音,恰好JavaScript语言又提供了相对可用的分号自动补全规则,所以,很多JavaScript的程序员都是倾向于不写分号。
|
||||
|
||||
这里要特意说一点,在今天的文章中,我并不希望去售卖自己的观点(其实我是属于“加分号”党),而是希望比较中立地给你讲清楚相关的知识,让你具备足够的判断力。
|
||||
|
||||
我们首先来了解一下自动插入分号的规则。
|
||||
|
||||
## 自动插入分号规则
|
||||
|
||||
自动插入分号规则其实独立于所有的语法产生式定义,它的规则说起来非常简单,只有三条。
|
||||
|
||||
- 要有换行符,且下一个符号是不符合语法的,那么就尝试插入分号。
|
||||
- 有换行符,且语法中规定此处不能有换行符,那么就自动插入分号。
|
||||
- 源代码结束处,不能形成完整的脚本或者模块结构,那么就自动插入分号。
|
||||
|
||||
这样描述是比较难以理解的,我们一起看一些实际的例子进行分析:
|
||||
|
||||
```
|
||||
let a = 1
|
||||
void function(a){
|
||||
console.log(a);
|
||||
}(a);
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,第一行的结尾处有换行符,接下来void关键字接在1之后是不合法的,这命中了我们的第一条规则,因此会在void前插入分号。
|
||||
|
||||
```
|
||||
var a = 1, b = 1, c = 1;
|
||||
a
|
||||
++
|
||||
b
|
||||
++
|
||||
c
|
||||
|
||||
```
|
||||
|
||||
这也是个著名的例子,我们看第二行的a之后,有换行符,后面遇到了++运算符,a后面跟++是合法的语法,但是我们看看JavaScript标准定义中,有[no LineTerminator here]这个字样,这是一个语法定义中的规则,你可以感受一下这个规则的内容(下一小节,我会给你详细介绍no LineTerminator here ):
|
||||
|
||||
```
|
||||
UpdateExpression[Yield, Await]:
|
||||
LeftHandSideExpression[?Yield, ?Await]
|
||||
LeftHandSideExpression[?Yield, ?Await][no LineTerminator here]++
|
||||
LeftHandSideExpression[?Yield, ?Await][no LineTerminator here]--
|
||||
++UnaryExpression[?Yield, ?Await]
|
||||
--UnaryExpression[?Yield, ?Await]
|
||||
|
||||
```
|
||||
|
||||
于是,这里a的后面就要插入一个分号了。所以这段代码最终的结果,b和c都变成了2,而a还是1。
|
||||
|
||||
```
|
||||
(function(a){
|
||||
console.log(a);
|
||||
})()
|
||||
(function(a){
|
||||
console.log(a);
|
||||
})()
|
||||
|
||||
```
|
||||
|
||||
这个例子是比较有实际价值的例子,这里两个function调用的写法被称作IIFE(立即执行的函数表达式),是个常见技巧。
|
||||
|
||||
这段代码意图上显然是形成两个IIFE。
|
||||
|
||||
我们来看第三行结束的位置,JavaScript引擎会认为函数返回的可能是个函数,那么,在后面再跟括号形成函数调用就是合理的,因此这里不会自动插入分号。
|
||||
|
||||
这是一些鼓励不写分号的编码风格会要求大家写IIFE时必须在行首加分号的原因。
|
||||
|
||||
```
|
||||
function f(){
|
||||
return/*
|
||||
This is a return value.
|
||||
*/1;
|
||||
}
|
||||
f();
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,return和1被用注释分隔开了。
|
||||
|
||||
根据JavaScript自动插入分号规则,**带换行符的注释也被认为是有换行符**,而恰好的是,return也有[no LineTerminator here]规则的要求。所以这里会自动插入分号,f执行的返回值是undefined。
|
||||
|
||||
## no LineTerminator here 规则
|
||||
|
||||
好了,到这里我们已经讲清楚了分号自动插入的规则,但是我们要想彻底掌握分号的奥秘,就必须要对JavaScript的语法定义做一些数据挖掘工作。
|
||||
|
||||
no LineTerminator here规则表示它所在的结构中的这一位置不能插入换行符。
|
||||
|
||||
自动插入分号规则的第二条:有换行符,且语法中规定此处不能有换行符,那么就自动插入分号。跟no LineTerminator here规则强相关,那么我们就找出JavaScript语法定义中的这些规则。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/ad/c3ffbc89e049ad1901d4108c8ad88aad.jpg" alt="">
|
||||
|
||||
为了方便你理解,我把产生式换成了实际的代码。
|
||||
|
||||
下面一段代码展示了,带标签的continue语句,不能在continue后插入换行。
|
||||
|
||||
```
|
||||
outer:for(var j = 0; j < 10; j++)
|
||||
for(var i = 0; i < j; i++)
|
||||
continue /*no LineTerminator here*/ outter
|
||||
|
||||
```
|
||||
|
||||
break跟continue是一样的,break后也不能插入换行:
|
||||
|
||||
```
|
||||
outer:for(var j = 0; j < 10; j++)
|
||||
for(var i = 0; i < j; i++)
|
||||
break /*no LineTerminator here*/ outter
|
||||
|
||||
```
|
||||
|
||||
我们前面已经提到过return和后自增、后自减运算符。
|
||||
|
||||
```
|
||||
function f(){
|
||||
return /*no LineTerminator here*/1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
i/*no LineTerminator here*/++
|
||||
i/*no LineTerminator here*/--
|
||||
|
||||
```
|
||||
|
||||
以及,throw和Exception之间也不能插入换行符:
|
||||
|
||||
```
|
||||
throw/*no LineTerminator here*/new Exception("error")
|
||||
|
||||
```
|
||||
|
||||
凡是async关键字,后面都不能插入换行符:
|
||||
|
||||
```
|
||||
async/*no LineTerminator here*/function f(){
|
||||
|
||||
}
|
||||
const f = async/*no LineTerminator here*/x => x*x
|
||||
|
||||
```
|
||||
|
||||
箭头函数的箭头前,也不能插入换行:
|
||||
|
||||
```
|
||||
const f = x/*no LineTerminator here*/=> x*x
|
||||
|
||||
```
|
||||
|
||||
yield之后,不能插入换行:
|
||||
|
||||
```
|
||||
function *g(){
|
||||
var i = 0;
|
||||
while(true)
|
||||
yield/*no LineTerminator here*/i++;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
到这里,我已经整理了所有标准中的no LineTerminator here规则,实际上,no LineTerminator here规则的存在,多数情况是为了保证自动插入分号行为是符合预期的,但是令人遗憾的是,JavaScript在设计的最初,遗漏了一些重要的情况,所以有一些不符合预期的情况出现,需要我们格外注意。
|
||||
|
||||
## 不写分号需要注意的情况
|
||||
|
||||
下面我们来看几种不写分号容易造成错误的情况,你可以稍微注意一下,避免发生同样的问题。
|
||||
|
||||
### 以括号开头的语句
|
||||
|
||||
我们在前面的案例中,已经展示了一种情况,那就是以括号开头的语句:
|
||||
|
||||
```
|
||||
(function(a){
|
||||
console.log(a);
|
||||
})()/*这里没有被自动插入分号*/
|
||||
(function(a){
|
||||
console.log(a);
|
||||
})()
|
||||
|
||||
```
|
||||
|
||||
这段代码看似两个独立执行的函数表达式,但是其实第三组括号被理解为传参,导致抛出错误。
|
||||
|
||||
### 以数组开头的语句
|
||||
|
||||
除了括号,以数组开头的语句也十分危险:
|
||||
|
||||
```
|
||||
var a = [[]]/*这里没有被自动插入分号*/
|
||||
[3, 2, 1, 0].forEach(e => console.log(e))
|
||||
|
||||
```
|
||||
|
||||
这段代码本意是一个变量a赋值,然后对一个数组执行forEach,但是因为没有自动插入分号,被理解为下标运算符和逗号表达式,我这个例子展示的情况,甚至不会抛出错误,这对于代码排查问题是个噩梦。
|
||||
|
||||
### 以正则表达式开头的语句
|
||||
|
||||
正则表达式开头的语句也值得你去多注意一下。我们来看这个例子。
|
||||
|
||||
```
|
||||
var x = 1, g = {test:()=>0}, b = 1/*这里没有被自动插入分号*/
|
||||
/(a)/g.test("abc")
|
||||
console.log(RegExp.$1)
|
||||
|
||||
```
|
||||
|
||||
这段代码本意是声明三个变量,然后测试一个字符串中是否含有字母a,但是因为没有自动插入分号,正则的第一个斜杠被理解成了除号,后面的意思就都变了。
|
||||
|
||||
注意,我构造的这个例子跟上面的例子一样,同样不会抛错,凡是这一类情况,都非常致命。
|
||||
|
||||
### 以Template开头的语句
|
||||
|
||||
以Template开头的语句比较少见,但是跟正则配合时,仍然不是不可能出现:
|
||||
|
||||
```
|
||||
|
||||
var f = function(){
|
||||
return "";
|
||||
}
|
||||
var g = f/*这里没有被自动插入分号*/
|
||||
`Template`.match(/(a)/);
|
||||
console.log(RegExp.$1)
|
||||
|
||||
```
|
||||
|
||||
这段代码本意是声明函数f,然后赋值给g,再测试Template中是否含有字母a。但是因为没有自动插入分号,函数f被认为跟Template一体的,进而被莫名其妙地执行了一次。
|
||||
|
||||
## 总结
|
||||
|
||||
这一节课,我们讨论了要不要加分号的问题。
|
||||
|
||||
首先我们介绍了自动插入分号机制,又对JavaScript语法中的no line terminator规则做了个整理,最后,我挑选了几种情况,为你介绍了不写分号需要注意的一些常见的错误。
|
||||
|
||||
最后留给你一个问题,请找一些开源项目,看看它们的编码规范是否要求加分号,欢迎留言讨论。
|
||||
438
极客时间专栏/重学前端/模块一:JavaScript/(小实验)理解编译原理:一个四则运算的解释器.md
Normal file
438
极客时间专栏/重学前端/模块一:JavaScript/(小实验)理解编译原理:一个四则运算的解释器.md
Normal file
@@ -0,0 +1,438 @@
|
||||
<audio id="audio" title="(小实验)理解编译原理:一个四则运算的解释器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1a/fe/1aec47c521826e8bb2fbec95d5b15cfe.mp3"></audio>
|
||||
|
||||
你好,我是winter。
|
||||
|
||||
在前面的课程中,我在JavaScript和CSS的部分,多次提到了编译原理相关的知识。这一部分的知识,如果我们从编译原理“龙书”等正规的资料中学习,就会耗费掉不少的时间,所以我在这里设计了一个小实验,帮助你快速理解编译原理相关的知识。
|
||||
|
||||
今天的内容比较特殊,我们来做一段详细的代码实验,详细的代码我放在了文章里,如果你正在收听音频,可以点击文章查看详情。
|
||||
|
||||
## 分析
|
||||
|
||||
按照编译原理相关的知识,我们来设计一下工作,这里我们分成几个步骤。
|
||||
|
||||
- 定义四则运算:产出四则运算的词法定义和语法定义。
|
||||
- 词法分析:把输入的字符串流变成token。
|
||||
- 语法分析:把token变成抽象语法树AST。
|
||||
- 解释执行:后序遍历AST,执行得出结果。
|
||||
|
||||
## 定义四则运算
|
||||
|
||||
四则运算就是加减乘除四种运算,例如:
|
||||
|
||||
```
|
||||
1 + 2 * 3
|
||||
|
||||
```
|
||||
|
||||
首先我们来定义词法,四则运算里面只有数字和运算符,所以定义很简单,但是我们还要注意空格和换行符,所以词法定义大概是下面这样的。
|
||||
|
||||
<li>Token
|
||||
<ul>
|
||||
- Number: `1` `2` `3` `4` `5` `6` `7` `8` `9` `0` 的组合
|
||||
- Operator: `+` 、`-`、 `*`、 `/` 之一
|
||||
|
||||
这里我们对空白和换行符没有任何的处理,所以词法分析阶段会直接丢弃。
|
||||
|
||||
接下来我们来定义语法,语法定义多数采用BNF,但是其实大家写起来都是乱写的,比如JavaScript标准里面就是一种跟BNF类似的自创语法。
|
||||
|
||||
不过语法定义的核心思想不会变,都是几种结构的组合产生一个新的结构,所以语法定义也叫语法产生式。
|
||||
|
||||
因为加减乘除有优先级,所以我们可以认为加法是由若干个乘法再由加号或者减号连接成的:
|
||||
|
||||
```
|
||||
<Expression> ::=
|
||||
<AdditiveExpression><EOF>
|
||||
|
||||
<AdditiveExpression> ::=
|
||||
<MultiplicativeExpression>
|
||||
|<AdditiveExpression><+><MultiplicativeExpression>
|
||||
|<AdditiveExpression><-><MultiplicativeExpression>
|
||||
|
||||
```
|
||||
|
||||
这种BNF的写法类似递归的原理,你可以理解一下,它表示一个列表。为了方便,我们把普通数字也得当成乘法的一种特例了。
|
||||
|
||||
```
|
||||
<MultiplicativeExpression> ::=
|
||||
<Number>
|
||||
|<MultiplicativeExpression><*><Number>
|
||||
|<MultiplicativeExpression></><Number>
|
||||
|
||||
```
|
||||
|
||||
好了,这就是四则运算的定义了。
|
||||
|
||||
## 词法分析:状态机
|
||||
|
||||
词法分析部分,我们把字符流变成token流。词法分析有两种方案,一种是状态机,一种是正则表达式,它们是等效的,选择你喜欢的就好,这里我都会你介绍一下状态机。
|
||||
|
||||
根据分析,我们可能产生四种输入元素,其中只有两种token,我们状态机的第一个状态就是根据第一个输入字符来判断进入了哪种状态:
|
||||
|
||||
```
|
||||
var token = [];
|
||||
const start = char => {
|
||||
if(char === '1'
|
||||
|| char === '2'
|
||||
|| char === '3'
|
||||
|| char === '4'
|
||||
|| char === '5'
|
||||
|| char === '6'
|
||||
|| char === '7'
|
||||
|| char === '8'
|
||||
|| char === '9'
|
||||
|| char === '0'
|
||||
) {
|
||||
token.push(char);
|
||||
return inNumber;
|
||||
}
|
||||
if(char === '+'
|
||||
|| char === '-'
|
||||
|| char === '*'
|
||||
|| char === '/'
|
||||
) {
|
||||
emmitToken(char, char);
|
||||
return start
|
||||
}
|
||||
if(char === ' ') {
|
||||
return start;
|
||||
}
|
||||
if(char === '\r'
|
||||
|| char === '\n'
|
||||
) {
|
||||
return start;
|
||||
}
|
||||
}
|
||||
const inNumber = char => {
|
||||
if(char === '1'
|
||||
|| char === '2'
|
||||
|| char === '3'
|
||||
|| char === '4'
|
||||
|| char === '5'
|
||||
|| char === '6'
|
||||
|| char === '7'
|
||||
|| char === '8'
|
||||
|| char === '9'
|
||||
|| char === '0'
|
||||
) {
|
||||
token.push(char);
|
||||
return inNumber;
|
||||
} else {
|
||||
emmitToken("Number", token.join(""));
|
||||
token = [];
|
||||
return start(char); // put back char
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个状态机非常简单,它只有两个状态,因为我们只有Number不是单字符的token。
|
||||
|
||||
这里我的状态机实现是非常经典的方式:用函数表示状态,用if表示状态的迁移关系,用return值表示下一个状态。
|
||||
|
||||
下面我们来运行一下这个状态机试试看:
|
||||
|
||||
```
|
||||
|
||||
function emmitToken(type, value) {
|
||||
console.log(value);
|
||||
}
|
||||
|
||||
var input = "1024 + 2 * 256"
|
||||
|
||||
var state = start;
|
||||
|
||||
for(var c of input.split(''))
|
||||
state = state(c);
|
||||
|
||||
state(Symbol('EOF'))
|
||||
|
||||
|
||||
```
|
||||
|
||||
运行后我们发现输出如下:
|
||||
|
||||
```
|
||||
1024
|
||||
+
|
||||
2
|
||||
*
|
||||
256
|
||||
|
||||
```
|
||||
|
||||
这是我们想要的答案。
|
||||
|
||||
## 语法分析:LL
|
||||
|
||||
做完了词法分析,我们开始进行语法分析,LL语法分析根据每一个产生式来写一个函数,首先我们来写好函数名:
|
||||
|
||||
```
|
||||
function AdditiveExpression( ){
|
||||
|
||||
|
||||
}
|
||||
function MultiplicativeExpression(){
|
||||
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
为了便于理解,我们就不做流式处理了,实际上一般编译代码都应该支持流式处理。
|
||||
|
||||
所以我们假设token已经都拿到了:
|
||||
|
||||
```
|
||||
var tokens = [{
|
||||
type:"Number",
|
||||
value: "1024"
|
||||
}, {
|
||||
type:"+"
|
||||
value: "+"
|
||||
}, {
|
||||
type:"Number",
|
||||
value: "2"
|
||||
}, {
|
||||
type:"*"
|
||||
value: "*"
|
||||
}, {
|
||||
type:"Number",
|
||||
value: "256"
|
||||
}, {
|
||||
type:"EOF"
|
||||
}];
|
||||
|
||||
```
|
||||
|
||||
每个产生式对应着一个函数,例如:根据产生式,我们的AdditiveExpression需要处理三种情况:
|
||||
|
||||
```
|
||||
<AdditiveExpression> ::=
|
||||
<MultiplicativeExpression>
|
||||
|<AdditiveExpression><+><MultiplicativeExpression>
|
||||
|<AdditiveExpression><-><MultiplicativeExpression>
|
||||
|
||||
```
|
||||
|
||||
那么AddititveExpression中就要写三个if分支,来处理三种情况。
|
||||
|
||||
AdditiveExpression的写法是根传入的节点,利用产生式合成新的节点
|
||||
|
||||
```
|
||||
function AdditiveExpression(source){
|
||||
if(source[0].type === "MultiplicativeExpression") {
|
||||
let node = {
|
||||
type:"AdditiveExpression",
|
||||
children:[source[0]]
|
||||
}
|
||||
source[0] = node;
|
||||
return node;
|
||||
}
|
||||
if(source[0].type === "AdditiveExpression" && source[1].type === "+") {
|
||||
let node = {
|
||||
type:"AdditiveExpression",
|
||||
operator:"+",
|
||||
children:[source.shift(), source.shift(), MultiplicativeExpression(source)]
|
||||
}
|
||||
source.unshift(node);
|
||||
}
|
||||
if(source[0].type === "AdditiveExpression" && source[1].type === "-") {
|
||||
let node = {
|
||||
type:"AdditiveExpression",
|
||||
operator:"-",
|
||||
children:[source.shift(), source.shift(), MultiplicativeExpression(source)]
|
||||
}
|
||||
source.unshift(node);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
那么下一步我们就把解析好的token传给我们的顶层处理函数Expression。
|
||||
|
||||
```
|
||||
Expression(tokens);
|
||||
|
||||
```
|
||||
|
||||
接下来,我们看Expression该怎么处理它。
|
||||
|
||||
我们Expression收到第一个token,是个Number,这个时候,Expression就傻了,这是因为产生式只告诉我们,收到了 AdditiveExpression 怎么办。
|
||||
|
||||
这个时候,我们就需要对产生式的首项层层展开,根据所有可能性调用相应的处理函数,这个过程在编译原理中称为求“closure”。
|
||||
|
||||
```
|
||||
function Expression(source){
|
||||
if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "EOF" ) {
|
||||
let node = {
|
||||
type:"Expression",
|
||||
children:[source.shift(), source.shift()]
|
||||
}
|
||||
source.unshift(node);
|
||||
return node;
|
||||
}
|
||||
AdditiveExpression(source);
|
||||
return Expression(source);
|
||||
}
|
||||
function AdditiveExpression(source){
|
||||
if(source[0].type === "MultiplicativeExpression") {
|
||||
let node = {
|
||||
type:"AdditiveExpression",
|
||||
children:[source[0]]
|
||||
}
|
||||
source[0] = node;
|
||||
return AdditiveExpression(source);
|
||||
}
|
||||
if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "+") {
|
||||
let node = {
|
||||
type:"AdditiveExpression",
|
||||
operator:"+",
|
||||
children:[]
|
||||
}
|
||||
node.children.push(source.shift());
|
||||
node.children.push(source.shift());
|
||||
MultiplicativeExpression(source);
|
||||
node.children.push(source.shift());
|
||||
source.unshift(node);
|
||||
return AdditiveExpression(source);
|
||||
}
|
||||
if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "-") {
|
||||
let node = {
|
||||
type:"AdditiveExpression",
|
||||
operator:"-",
|
||||
children:[]
|
||||
}
|
||||
node.children.push(source.shift());
|
||||
node.children.push(source.shift());
|
||||
MultiplicativeExpression(source);
|
||||
node.children.push(source.shift());
|
||||
source.unshift(node);
|
||||
return AdditiveExpression(source);
|
||||
}
|
||||
if(source[0].type === "AdditiveExpression")
|
||||
return source[0];
|
||||
MultiplicativeExpression(source);
|
||||
return AdditiveExpression(source);
|
||||
}
|
||||
function MultiplicativeExpression(source){
|
||||
if(source[0].type === "Number") {
|
||||
let node = {
|
||||
type:"MultiplicativeExpression",
|
||||
children:[source[0]]
|
||||
}
|
||||
source[0] = node;
|
||||
return MultiplicativeExpression(source);
|
||||
}
|
||||
if(source[0].type === "MultiplicativeExpression" && source[1] && source[1].type === "*") {
|
||||
let node = {
|
||||
type:"MultiplicativeExpression",
|
||||
operator:"*",
|
||||
children:[]
|
||||
}
|
||||
node.children.push(source.shift());
|
||||
node.children.push(source.shift());
|
||||
node.children.push(source.shift());
|
||||
source.unshift(node);
|
||||
return MultiplicativeExpression(source);
|
||||
}
|
||||
if(source[0].type === "MultiplicativeExpression"&& source[1] && source[1].type === "/") {
|
||||
let node = {
|
||||
type:"MultiplicativeExpression",
|
||||
operator:"/",
|
||||
children:[]
|
||||
}
|
||||
node.children.push(source.shift());
|
||||
node.children.push(source.shift());
|
||||
node.children.push(source.shift());
|
||||
source.unshift(node);
|
||||
return MultiplicativeExpression(source);
|
||||
}
|
||||
if(source[0].type === "MultiplicativeExpression")
|
||||
return source[0];
|
||||
|
||||
return MultiplicativeExpression(source);
|
||||
};
|
||||
|
||||
var source = [{
|
||||
type:"Number",
|
||||
value: "3"
|
||||
}, {
|
||||
type:"*",
|
||||
value: "*"
|
||||
}, {
|
||||
type:"Number",
|
||||
value: "300"
|
||||
}, {
|
||||
type:"+",
|
||||
value: "+"
|
||||
}, {
|
||||
type:"Number",
|
||||
value: "2"
|
||||
}, {
|
||||
type:"*",
|
||||
value: "*"
|
||||
}, {
|
||||
type:"Number",
|
||||
value: "256"
|
||||
}, {
|
||||
type:"EOF"
|
||||
}];
|
||||
var ast = Expression(source);
|
||||
|
||||
console.log(ast);
|
||||
|
||||
```
|
||||
|
||||
## 解释执行
|
||||
|
||||
得到了AST之后,最困难的一步我们已经解决了。这里我们就不对这颗树做任何的优化和精简了,那么接下来,直接进入执行阶段。我们只需要对这个树做遍历操作执行即可。
|
||||
|
||||
我们根据不同的节点类型和其它信息,写if分别处理即可:
|
||||
|
||||
```
|
||||
|
||||
function evaluate(node) {
|
||||
if(node.type === "Expression") {
|
||||
return evaluate(node.children[0])
|
||||
}
|
||||
if(node.type === "AdditiveExpression") {
|
||||
if(node.operator === '-') {
|
||||
return evaluate(node.children[0]) - evaluate(node.children[2]);
|
||||
}
|
||||
if(node.operator === '+') {
|
||||
return evaluate(node.children[0]) + evaluate(node.children[2]);
|
||||
}
|
||||
return evaluate(node.children[0])
|
||||
}
|
||||
if(node.type === "MultiplicativeExpression") {
|
||||
if(node.operator === '*') {
|
||||
return evaluate(node.children[0]) * evaluate(node.children[2]);
|
||||
}
|
||||
if(node.operator === '/') {
|
||||
return evaluate(node.children[0]) / evaluate(node.children[2]);
|
||||
}
|
||||
return evaluate(node.children[0])
|
||||
}
|
||||
if(node.type === "Number") {
|
||||
return Number(node.value);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
在这个小实验中,我们通过一个小实验学习了编译原理的基本知识,小实验的目的是帮助你理解JavaScript课程中涉及到的编译原理基本概念,它离真正的编译原理学习还有很大的差距。
|
||||
|
||||
通过实验,我们了解了产生式、词法分析、语法分析和解释执行的过程。
|
||||
|
||||
最后留给你一些挑战,你可以根据自己的水平选择:
|
||||
|
||||
- 补全emmitToken,使得我们的代码能完整工作起来。
|
||||
- 为四则运算加入小数。
|
||||
- 引入负数。
|
||||
- 添加括号功能。
|
||||
|
||||
欢迎写好的同学留言给我。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user