深入理解ES6-块级作用域和字符串

本篇整理了《深入理解ES6》书中的块级作用域和字符串章节中的重要知识点。

块级作用域绑定

var声明及变量提升(Hoisting)机制

通过关键字var声明的变量,无论是在哪声明的,都会被提升到当前作用域的顶部,而初始化操作依旧留在原处执行。

块级声明

块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域(词法作用域)存在于:

  • 函数内部
  • 块中({}之间的区域)

let声明

声明变量
变量作用域限定在当前代码块中
声明不会被提升
同一作用域中不能重复声明

const声明

声明常量,一旦设定变不可更改(意味着不能重新赋值,如果声明的是对象,对象属性值是可以修改的)
所声明的常量必须进行初始化
声明不会被提升
同一作用域中不能重复声明

临时死区(Temporal Dead Zone)

Javascript 引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到 var 声明),要么将声明放到TDZ中(遇到 let 和 const 声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可正常访问。

1
2
3
4
if(condition){
console.log(typeof value); // 引用错误!
let value = "a";
}

是因为value在TDZ中,即使不易出错的typeof也无法阻挡引擎抛出错误。

1
2
3
4
console.log(typeof value); // "undefined"
if(condition){
let value = "a";
}

是因为typeof value是在TDZ外执行的。

循环中的块作用域绑定

循环中的let声明

每次迭代循环都创建一个新变量,并以之前迭代中同名变量的值将其初始化。而 var 每次迭代同时共享着变量。还是把经典的例子拿出来吧

1
2
3
4
5
6
7
8
9
10
11
var funcs = [];
for(var i=0; i<10; i++){
funcs.push(function(){
console.log(i);
});
}
funcs.forEach(function(func){
func(); // 输出10次10
});

上面例子只需把var 修改为 let 就可以像期望的那样运行,输出 0,1,2直到9

循环中const声明

普通for循环中,可以在初始化变量时使用const,但是更改这个变量的值就会抛出错误。
在for-in 或 for-of 中使用const的行为与使用let一致,是因为每次迭代不会修改已有绑定,而是创建一个新绑定。

全局作用域绑定

var被用于全局作用域时,它会创建一个新的全局变量作为全局对象(浏览器环境中的window对象)的属性。因此它会覆盖全局变量。

如果在全局作用域中使用let或者const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。因此不会覆盖全局变量。

1
2
var RegExp = "hello";
console.log(window.RegExp); //hello
1
2
3
let RegExp = "hello";
console.log(RegExp); //"hello"
console.log(window.RegExp); //undefined

块级绑定最佳实践的进化

默认使用const,只有确实需要改变变量的值时使用let。因为大部分变量的值在初始化不应再改变,而预料外的变量值的改变是很多bug的源头。

字符串和正则表达式

更好的Unicode支持

Javascript字符串一直基于16位字符编码(UTF-16)进行构建。每16位的序列是一个编码单元(code unit),代表一个字符。length、chartAt()等字符串属性和方法都是基于这种编码单元构造的。在过去16位足以包含任何字符,知道Unicode引入扩展字符集,编码规则才不得不进行变更。

UTF-16码位

在UTF-16中,前2^16个码位均以16位的编码单元表示,这个范围被称作基本多文种平面(BMP)。超出这个 范围的码位则要归属于某个辅助平面,其中的码位仅用16位就无法表示了。为此,UTF-16引入了代理对,其规定用两个16位编码单元表示一个码位。这也就是说字符串里的字符有两种,一种是由一个编码单元16位表示的BMP字符,另一种是由两个编码单元32位表示的辅助平面字符

在ES5中,所有字符串的操作都是机缘16位编码单元。如果采用同样的方式处理包含代理对的UTF-16位编码字符,得到的结果可能与预期不符

1
2
3
4
5
6
7
8
let text = "𠮷";
console.log(text.length); // 2
console.log(/^.$/.test(text)); // false
console.log(text.charAt(0)); // ""
console.log(text.charAt(1)); // ""
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 55271

Unicode字符”𠮷”是通过代理对来表示的,因此,这个示例中的JavaScript字符串操作将其视为两个16位字符。这就意味着:

  • 变量text的长度事实上是1,但它的length属性值却为2。
  • 变量text被判定为两个字符,因此匹配单一字符的正则表达式会失效。
  • 前后两个16位的编码单元都不表示任何可打印的字符,因此charAt()方法不会返回合法的字符串。
  • chartCodeAt()方法同样不能正确地识别字符。它会返回每个16位编码单元对应的数值,在ES5中,这是你最接近真实值得结果了。

ES6中增加了专门针对代理对的功能。

codePointAt()方法

参数为编码单元位置,而非字符位置作为参数,返回与字符串中给定位置对应的码位,即一个整数值。

1
2
3
4
5
6
7
8
9
let text = "𠮷a";
console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 55271
console.log(text.charCodeAt(2)); // 55362
console.log(text.codePointAt(0)); // 55362
console.log(text.codePointAt(1)); // 55271
console.log(text.codePointAt(2)); // 55362

上面代码中,JavaScript将”𠮷a”视为三个字符,codePointAt方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点134071(即十六进制的20BB7)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt方法的结果与charCodeAt方法相同。

检测一个字符占用的编码单元数量,最简单的方法是调用codePointAt方法

1
2
3
4
5
6
function is32Bit(c){
return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("𠮷")); // true
console.log(is32Bit("a")); // false

String.fromCodePoint()方法

ES5提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别32位的UTF-16字符(Unicode编号大于0xFFFF)。

ES6提供了String.fromCodePoint方法,可以识别0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

normalize()方法

许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。

这两种表示方法,在视觉和语义上都等价,但是JavaScript不能识别。

1
2
3
console.log('\u01D1'==='\u004F\u030C'); //false
console.log('\u01D1'.length); // 1
console.log('\u004F\u030C'.length); // 2

上面代码表示,JavaScript将合成字符视为两个字符,导致两种表示方法不相等。

ES6提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。

1
console.log('\u01D1'.normalize() === '\u004F\u030C'.normalize()); // true

normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。

  • NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
  • NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。
  • NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)
  • NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。

不过,normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过Unicode编号区间判断。

正则表达式 u 修饰符

当一个正则表达式添加了u修饰符,它就从编码单元操作模式切换为字符模式,如此一来正则表达式就不会视代理对为两个字符,从而完全按照预期正常运行。

1
2
3
4
let text = "𠮷";
console.log(/^.$/.test(text)); // false
console.log(/^.$/u.test(text)); // true

其它字符串变更

字符串中的子串识别

  • includes()方法
  • startsWith()方法
  • endsWith()方法

第一个参数指定要搜索的文本;第二个参数是可选的,指定一个开始搜索的位置的索引值。

repeat()方法

其它正则表达式语法变更

y 修饰符

它会影响正则表达式搜索过程中的sticky属性,当在字符串中开始字符串匹配时,它会通知搜索从正则表达式的lastIndex属性开始进行,如果在指定位置没能成功匹配,则停止继续匹配。

正则表达式的复制

ES5中,可以通过给RegExp构造函数传递正则表达式作为参数来复制这个正则表达式,就像这样:

1
2
var re1 = /ab/i,
re2 = new RegExp(re1);

此处的变量re2只是re1的一份拷贝,如果给RegExp构造函数提供第二个参数,为正则表达式指定一个修饰符,则代码无法运行。

ES6中修改了这个行为,可以通过第二个参数来修改其修饰符了。

flags属性

ES5获取flag的方式

1
2
3
4
5
6
7
function getFlags(re){
var text = rs.toString();
return text.substring(text.lastIndexOf("/") + 1, text.length);
}
var re = /ab/g;
console.log(getFlags(re)); // "g"

为了使获取修饰符更加简单,ES6新增了一个flags属性;

模板字面量

基础语法

使用反撇号(`)来定义字符串

多行字符串

1
2
3
4
let message = `hello
world`;
console.log(message); // "hello"
// " world"

字符串占位符

占位符由一个左侧的${和右侧的}符号组成,中间可以包含任意的Javascript表达式。

1
2
let name = "Nicholas",
message = "Hello, ${name}";

标签模板

标签指的是在模板字面量第一个反撇号前方标注的字符串,就像这样:

1
let message = tag`Hello World`;

定义标签

标签可以是一个函数,调用时传入加工过的模板字面量各部分数据,但必须结合每个部分来创建结果。第一个参数是一个数组,包含JavaScript解释过后的字面量字符串,它之后的所有参数都是每一个占位符的解释值。

1
2
3
4
5
6
7
8
9
function tag(arr, ...values){
console.log(arr); // ["1+2=", "。"]
console.log(values); // [3]
}
let a = 1,
b = 2;
tag`1+2=${a+b}。`;

可以通过arr和values来重新组合回结果。