天天看点

重学前端3 - JavaScript

JavaScript

重学前端3 - JavaScript

05 | JavaScript类型:关于类型,有哪些你不知道的细节?

问题
  • 为什么有的编程规范要求用 void 0 代替 undefined?
  • 字符串有最大长度吗?
  • 0.1 + 0.2 不是等于 0.3 么?为什么 JavaScript 里不是这样的?
  • ES6 新加入的 Symbol 是个什么东西?
  • 为什么给对象添加的方法能用在基本类型上?
7 种语言类型

Undefined; Null; Boolean; String; Number; Symbol; Object

为什么有的编程规范要求用 void 0 代替 undefined?

因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用 void 0 来获取 undefined 值。

字符串是否有最大长度

String 用于表示文本数据。String 有最大长度是 2^53 - 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解中的字符数。

因为 String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

为什么在 JavaScript 中,0.1+0.2 不能等于0.3

这里输出的结果是 false,说明两边不相等的,这是浮点运算的特点,也是很多同学疑惑的来源,浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。

所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用 JavaScript 提供的最小精度值:

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
// 检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。
           
Symbol

Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。

Object

在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。

JavaScript 中的几个基本类型

  • Number
  • String
  • Boolean
  • Symbol
为什么给对象添加的方法能用在基本类型上?

.运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

StringToNumber

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

NumberToString

数字到字符串的转换是完全符合你直觉的十进制表示,当 Number 绝对值较大或者较小时,字符串表示则是使用科学计数法表示的

装箱转换

把基本类型转换为对应的对象

拆箱转换

对象类型到基本类型的转换

问题

最后我们留一个实践问题,如果我们不用原生的 Number 和 parseInt,用 JS 代码实现 String 到 Number 的转换,该怎么做呢?

06 | JavaScript对象:面向对象还是基于对象?

什么是面向对象?
  • 一个可以触摸或者可以看见的东西;
  • 人的智力可以理解的东西;
  • 可以指导思考或行动(进行想象或施加动作)的东西。
JavaScript 对象的特征
  • 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
  • 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
唯一标识性

各种语言的对象唯一标识性都是用内存地址来体现的,对象具有唯一标识的内存地址,所以具有唯一的标识。

状态和行为

JavaScript 中的行为和状态都能用属性来抽象

JavaScript 对象的两类属性

先来说第一类属性,数据属性。

  • value:就是属性的值。
  • writable:决定属性能否被赋值。
  • enumerable:决定 for in 能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。

第二类属性是访问器(getter/setter)属性

  • getter:函数或 undefined,在取属性值时被调用。
  • setter:函数或 undefined,在设置属性值时被调用。
  • enumerable:决定 for in 能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。

这样,我们就理解了,实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。

为什么会有“JavaScript 不是面向对象”这样的说法

这是由于 JavaScript 的对象设计跟目前主流基于类的面向对象差异非常大。

可事实上,这样的对象系统设计虽然特别,但是 JavaScript 提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式(基于类和基于原型),所以它也是正统的面向对象语言。

小结

JavaScript 对象的具体设计:具有高度动态性的属性集合。

07 | JavaScript对象:我们真的需要模拟类吗?

  • 基于类的面向对象。C++、Java
  • 基于原型的面向对象。JavaScript
原型
  • 照猫画虎
  • Object.create 来创建对象
ES6 中的类
  • class 关键字
  • get / set 关键字:创建 getter
  • 括号和大括号:创建方法
  • 数据型成员:构造器里面
  • extends 关键字(继承)
类与函数
  • class
  • function (=>)

08 | JavaScript对象:你知道全部的对象分类吗?

JavaScript 中的对象分类
  • 宿主对象(host Objects):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。
  • 内置对象(Built-in Objects):由 JavaScript 语言提供的对象。
    • 固有对象(Intrinsic Objects ):由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
    • 原生对象(Native Objects):可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
    • 普通对象(Ordinary Objects):由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。
用对象来模拟函数与构造器:函数对象与构造器对象
  • 函数对象的定义是:具有 [[call]] 私有字段的对象
  • 构造器对象的定义是:具有私有字段 [[construct]] 的对象

16 | JavaScript执行(一):Promise里的代码为什么比setTimeout先执行?

宏观和微观任务

这里每次的执行过程,其实都是一个宏观任务。我们可以大概理解:宏观任务的队列就相当于事件循环。

在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列:

重学前端3 - JavaScript

有了宏观任务和微观任务机制,我们就可以实现 JS 引擎级和宿主级的任务了,例如:

  • Promise 永远在队列尾部添加微观任务。
  • setTimeout 等宿主 API,则会添加宏观任务。
Promise

Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。

通过一系列的实验,我们可以总结一下如何分析异步执行的顺序:

  • 首先我们分析有多少个宏任务;
  • 在每个宏任务中,分析有多少个微任务;
  • 根据调用次序,确定宏任务中的微任务执行次序;
  • 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
  • 确定整个顺序。
新特性:async/await

async/await 是 ES2016 新加入的特性,它提供了用 for、if 等代码结构来编写异步的方式。它的运行时基础是 Promise,面对这种比较新的特性,我们先来看一下基本用法。

async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。

async 函数是一种特殊语法,特征是在 function 关键字之前加上 async 关键字,这样,就定义了一个 async 函数,我们可以在其中使用 await 来等待一个 Promise。

async 函数强大之处在于,它是可以嵌套的。我们在定义了一批原子操作的情况下,可以利用 async 函数组合出新的 async 函数。

结语

把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。许多的微观任务的队列组成了宏观任务。

除此之外,我们还展开介绍了用 Promise 来添加微观任务的方式,并且介绍了 async/await 这个语法的改进。

17 | JavaScript执行(二):闭包和执行上下文到底是怎么回事?

  • 闭包
  • 执行上下文:执行的基础设施
  • let
  • Realm

18 | JavaScript执行(三):你知道现在有多少种函数吗?

函数
  • 第一种,普通函数:用 function 关键字定义的函数。
  • 第二种,箭头函数:用 => 运算符定义的函数。
  • 第三种,方法:在 class 中定义的函数。
  • 第四种,生成器函数:用 function * 定义的函数。
  • 第五种,类:用 class 定义的类,实际上也是函数。
  • 第六种,异步函数:普通函数加上 async 关键字。
  • 第七种,异步函数:箭头函数加上 async 关键字。
  • 第八种,异步函数:生成器函数加上 async 关键字。
this 关键字的行为
  • this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不同,得到的 this 值也不同。
  • 调用函数时使用的引用,决定了函数执行时刻的 this 值。
  • 我们看到,改为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。
  • 异步普通函数跟普通函数行为是一致的
  • 生成器函数、异步生成器函数跟普通函数行为是一致的
  • 异步箭头函数与箭头函数行为是一致的
this 关键字的机制

JavaScript 用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表。如下图所示:

重学前端3 - JavaScript

当函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。

而 this 则是一个更为复杂的机制,JavaScript 标准定义了 [[thisMode]] 私有属性。

[[thisMode]] 私有属性有三个取值。

  • lexical:表示从上下文中找 this,这对应了箭头函数。
  • global:表示当 this 为 undefined 时,取全局对象,对应了普通函数。
  • strict:当严格模式时使用,this 严格按照调用时传入的值,可能为 null 或者 undefined。
操作 this 的内置函数

Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值。

此外,还有 Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的 this 值固定了参数。

new 与 this

我们在之前的对象部分已经讲过 new 的执行过程,我们再来看一下:

  • 以构造器的 prototype 属性(注意与私有字段 [[prototype]] 的区分)为原型,创建新对象;
  • 将 this 和调用参数传给构造器,执行;
  • 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。

显然,通过 new 调用函数,跟直接调用的 this 取值有明显区别。那么我们今天讲的这些函数跟 new 搭配又会产生什么效果呢?

重学前端3 - JavaScript

我们可以看到,仅普通函数和类能够跟 new 搭配使用。

19 | JavaScript执行(四):try里面放return,finally还会执行吗?

Completion 类型

Completion Record 表示一个语句执行完之后的结果,它有三个字段:

  • [[type]] 表示完成的类型,有 break continue return throw 和 normal 几种类型;
  • [[value]] 表示语句的返回值,如果语句没有,则是 empty;
  • [[target]] 表示语句的目标,通常是一个 JavaScript 标签(标签在后文会有介绍)。
语句
重学前端3 - JavaScript
语句块

语句块就是拿大括号括起来的一组语句,它是一种语句的复合结构,可以嵌套。

控制型语句

控制型语句带有 if、switch 关键字,它们会对不同类型的 Completion Record 产生反应。

控制类语句分成两部分,一类是对其内部造成影响,如 if、switch、while/for、try。另一类是对外部造成影响如 break、continue、return、throw,这两类语句的配合,会产生控制代码执行顺序和执行逻辑的效果,这也是我们编程的主要工作。

一般来说, for/while - break/continue 和 try - throw 这样比较符合逻辑的组合,是大家比较熟悉的,但是,实际上,我们需要控制语句跟 break 、continue 、return 、throw 四种类型与控制语句两两组合产生的效果。

重学前端3 - JavaScript
带标签的语句

前文我重点讲了 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 的循环语句会消费它。

26 | JavaScript词法:为什么12.toString会报错?

文法是编译原理中对语言的写法的一种规定,一般来说,文法分成词法和语法两种。

词法规定了语言的最小语义单元:token,可以翻译成“标记”或者“词”,在我的专栏文章中,我统一把 token 翻译成词。

  • WhiteSpace 空白字符
  • LineTerminator 换行符
  • Comment 注释
  • Token 词
    • IdentifierName 标识符名称,典型案例是我们使用的变量名,注意这里关键字也包含在内了。
    • Punctuator 符号,我们使用的运算符和大括号等符号。
    • NumericLiteral 数字直接量,就是我们写的数字。
    • StringLiteral 字符串直接量,就是我们用单引号或者双引号引起来的直接量。
    • Template 字符串模板,用反引号 ` 括起来的直接量。

27 |(小实验)理解编译原理:一个四则运算的解释器

按照编译原理相关的知识,我们来设计一下工作,这里我们分成几个步骤。

  • 定义四则运算:产出四则运算的词法定义和语法定义。
  • 词法分析:把输入的字符串流变成 token。
  • 语法分析:把 token 变成抽象语法树 AST。
  • 解释执行:后序遍历 AST,执行得出结果。

28 | JavaScript语法(预备篇):到底要不要写分号呢?

自动插入分号规则

自动插入分号规则其实独立于所有的语法产生式定义,它的规则说起来非常简单,只有三条。

  • 要有换行符,且下一个符号是不符合语法的,那么就尝试插入分号。
  • 有换行符,且语法中规定此处不能有换行符,那么就自动插入分号。
  • 源代码结束处,不能形成完整的脚本或者模块结构,那么就自动插入分号。
no LineTerminator here 规则

no LineTerminator here 规则表示它所在的结构中的这一位置不能插入换行符。

自动插入分号规则的第二条:有换行符,且语法中规定此处不能有换行符,那么就自动插入分号。跟 no LineTerminator here 规则强相关,那么我们就找出 JavaScript 语法定义中的这些规则。

重学前端3 - JavaScript
不写分号需要注意的情况
  • 以括号开头的语句
  • 以数组开头的语句
  • 以正则表达式开头的语句
  • 以 Template 开头的语句

29 | JavaScript语法(一):在script标签写export为什么会抛错?

脚本和模块

模块和脚本之间的区别仅仅在于是否包含 import 和 export。

重学前端3 - JavaScript
函数体

函数体实际上有四种

  • 普通函数体,例如:
function foo(){
    //Function body
}

           
  • 异步函数体,例如:
async function foo(){
    //Function body
}

           
  • 生成器函数体,例如:
function *foo(){
    //Function body
}

           
  • 异步生成器函数体,例如:
async function *foo(){
    //Function body
}

           

上面四种函数体的区别在于:能否使用 await 或者 yield 语句。

关于函数体、模块和脚本能使用的语句,我整理了一个表格,你可以参考一下:

重学前端3 - JavaScript
预处理

JavaScript 执行前,会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提前处理 var、函数声明、class、const 和 let 这些语句,以确定其中变量的意义。

  • var 声明

立即执行的函数表达式(IIFE)

  • function 声明
  • class 声明
指令序言机制

脚本和模块都支持一种特别的语法,叫做指令序言(Directive Prologs)。

这里的指令序言最早是为了 use strict 设计的,它规定了一种给 JavaScript 代码添加元信息的方式。

30 | JavaScript语法(二):你知道哪些JavaScript语句?

普通语句:
重学前端3 - JavaScript
声明型语句:
重学前端3 - JavaScript

31 | JavaScript语法(三):什么是表达式语句?

什么是表达式语句

表达式语句实际上就是一个表达式,它是由运算符连接变量或者直接量构成的。

一般来说,我们的表达式语句要么是函数调用,要么是赋值,要么是自增、自减,否则表达式计算的结果没有任何意义。

PrimaryExpression 主要表达式

首先我们来给你讲解一下表达式的原子项:Primary Expression。它是表达式的最小单位,它所涉及的语法结构也是优先级最高的。

Primary Expression 包含了各种“直接量”,直接量就是直接用某种语法写出来的具有特定类型的值。我们已经知道,在运行时有各种值,比如数字 123,字符串 Hello world,所以通俗地讲,直接量就是在代码中把它们写出来的语法。

我们在类型部分,已经介绍过一些基本类型的直接量。比如,我们当时用 null 关键字获取 null 值,这个用法就是 null 直接量,这就是这里我们仅仅把它们简单回顾一下:

"abc";
123;
null;
true;
false;
           

除这些之外,JavaScript 还能够直接量的形式定义对象,针对函数、类、数组、正则表达式等特殊对象类型,JavaScript 提供了语法层面的支持。

({});
(function(){});
(class{ });
[];
/abc/g;
           

需要注意,在语法层面,function、{ 和 class 开头的表达式语句与声明语句有语法冲突,所以,我们要想使用这样的表达式,必须加上括号来回避语法冲突。

Primary Expression 还可以是 this 或者变量,在语法上,把变量称作“标识符引用”。

this;
myVar;
           

任何表达式加上圆括号,都被认为是 Primary Expression,这个机制使得圆括号成为改变运算优先顺序的手段。

MemberExpression 成员表达式

Member Expression 通常是用于访问对象成员的。它有几种形式:

a.b;  // 用标识符的属性访问
a["b"];  // 用字符串的属性访问
new.target;  // 用于判断函数是否是被 new 调用
super.b;  // 用于访问父类的属性的语法
           

从名字就可以看出,Member Expression 最初设计是为了属性访问的,不过从语法结构需要,以下两种在 JavaScript 标准中当做 Member Expression:

f`a${b}c`;  // 这是一个是带函数的模板,这个带函数名的模板表示把模板的各个部分算好后传递给一个函数。

new Cls();  // 另一个是带参数列表的 new 运算,注意,不带参数列表的 new 运算优先级更低,不属于 Member Expression。
           
NewExpression NEW 表达式

这种非常简单,Member Expression 加上 new 就是 New Expression(当然,不加 new 也可以构成 New Expression,JavaScript 中默认独立的高优先级表达式都可以构成低优先级表达式)。

CallExpression 函数调用表达式

除了 New Expression,Member Expression 还能构成 Call Expression。它的基本形式是 Member Expression 后加一个括号里的参数列表,或者我们可以用上 super 关键字代替 Member Expression。

LeftHandSideExpression 左值表达式

接下来,我们需要理解一个概念:New Expression 和 Call Expression 统称 LeftHandSideExpression,左值表达式。

我们直观地讲,左值表达式就是可以放到等号左边的表达式。JavaScript 语法则是下面这样。

AssignmentExpression 赋值表达式

AssignmentExpression 赋值表达式也有多种形态,最基本的当然是使用等号赋值。

Expression 表达式

在 JavaScript 中,比赋值运算优先级更低的就是逗号运算符了。我们可以把逗号可以理解为一种小型的分号。

逗号分隔的表达式会顺次执行,就像不同的表达式语句一样。“整个表达式的结果”就是“最后一个逗号后的表达式结果”。

32 | JavaScript语法(四):新加入的**运算符,哪里有些不一样呢?

更新表达式 UpdateExpression

左值表达式搭配 ++、-- 运算符,可以形成更新表达式。

-- a;
++ a;
a --
a ++
           

更新表达式会改变一个左值表达式的值。分为前后自增,前后自减一共四种。

一元运算表达式 UnaryExpression
delete a.b;
void a;
typeof a;
- a;
~ a;
! a;
await a;
           
乘方表达式 ExponentiationExpression

乘方表达式也是由更新表达式构成的。它使用 ** 号。

++i ** 30
2 ** 30 // 正确
-2 ** 30 // 报错
           
乘法表达式 MultiplicativeExpression

乘法表达式有三种运算符:

*
/
%
           
加法表达式 AdditiveExpression

加法表达式 AdditiveExpression

+ 
-
           
移位表达式 ShiftExpression

移位表达式由加法表达式构成,移位是一种位运算,分成三种:

<< 向左移位
>> 向右移位
>>> 无符号向右移位
           

移位运算把操作数看做二进制表示的整数,然后移动特定位数。所以左移 n 位相当于乘以 2 的 n 次方,右移 n 位相当于除以 2 取整 n 次。

普通移位会保持正负数。无符号移位会把减号视为符号位 1,同时参与移位:

关系表达式 RelationalExpression

移位表达式可以构成关系表达式,这里的关系表达式就是大于、小于、大于等于、小于等于等运算符号连接,统称为关系运算。

  • <=
  • =
  • <
  • instanceof
  • in

需要注意,这里的 <= 和 >= 关系运算,完全是针对数字的,所以 <= 并不等价于 < 或 ==。例如

null <= undefined
//false
null == undefined
//true
           
相等表达式 EqualityExpression

相等表达式由四种运算符和关系表达式构成,我们来看一下运算符:

  • ==
  • !=
  • ===
  • !==

虽然标准中写的 == 十分复杂,但是归根结底,类型不同的变量比较时

  • 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 类型之间时使用,比如:

在这个例子中,等号左边必然是 string,右边的直接量必然是 number,这样使用 == 就没有问题了。

位运算表达式

位运算表达式含有三种:

  • 按位与表达式 BitwiseANDExpression (&)
  • 按位异或表达式 BitwiseANDExpression (^)
  • 按位或表达式 BitwiseORExpression (|)

异或运算来交换两个整数的值

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
           

继续阅读