本文继续对JavaScript高级程序设计第四版 第三章 语言基础 进行学习
一、3.1节 语法
1.加分号
即使语句末尾的分号不是必需的,也应该加上。记着加分号有助于防止省略造成的问题,比如可以避免输入内容不完整。此外,加分号也便于开发者通过删除空行来压缩代码(如果没有结尾的分号,只删除空行,则会导致语法错误)。加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。
2.使用代码块
if 之类的控制语句只在执行多条语句时要求必须有代码块。不过,最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句,如下例所示:
// 有效,但容易导致错误,应该避免
if (test)
console.log(test);
// 推荐
if (test) {
console.log(test);
}
在控制语句中使用代码块可以让内容更清晰,在需要修改代码时也可以减少出错的可能性。
二、3.2节 关键字与保留字
三、3.3节 var let const
ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:var、const 和 let。其中,var 在ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。
1. var 声明作用域
关键的问题在于,使用 var 操作符定义的变量会成为包含它的函数的局部变量。比如,使用 var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
这里,message 变量是在函数内部使用 var 定义的。函数叫 test(),调用它会创建这个变量并给它赋值。调用之后变量随即被销毁,因此示例中的最后一行会导致错误。不过,在函数内定义变量时省略 var 操作符,可以创建一个全局变量:
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
去掉之前的 var 操作符之后,message 就变成了全局变量。只要调用一次函数 test(),就会定义这个变量,并且可以在函数外部访问到。
2. var 声明提升
使用 var 时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
之所以不会报错,是因为 ECMAScript 运行时把它看成等价于如下代码:
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用 var 声明同一个变量也没有问题:
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
3.let 声明
let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。
// name 会被提升
console.log(name); // undefined
var name = 'Matt';
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;
4. for 循环中的 let 声明
在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部:
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部:
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义
在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 你可能以为会输出 0、1、2、3、4
// 实际上会输出 5、5、5、5、5
之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。
而在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 会输出 0、1、2、3、4
这种每次迭代声明一个独立变量实例的行为适用于所有风格的 for 循环,包括 for-in 和 for-of循环。
5.const 声明
const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。
const age = 26;
age = 36; // TypeError: 给常量赋值
const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反 const 的限制。
const person = {};
person.name = 'Matt'; // ok
6.声明风格及最佳实践
ECMAScript 6 增加 let 和 const 从客观上为这门语言更精确地声明作用域和语义提供了更好的支持。行为怪异的 var 所造成的各种问题,已经让 JavaScript 社区为之苦恼了很多年。随着这两个新关键字的出现,新的有助于提升代码质量的最佳实践也逐渐显现。
- 不使用 var
有了 let 和 const,大多数开发者会发现自己不再需要 var 了。限制自己只使用 let 和 const有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。 -
const 优先,let 次之
使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,只在提前知道未来会有修改时,再使用 let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为。
四、3.4.1节 typeof instanceof
typeof操作符是一个确定变量是字符串、数值、布尔值,还是undefined的最佳工具。如果变量的值是一个对象或null,则返回Object.虽然在检测基本数据类型时typeof是个给力的助手,但是在检测引用类型的值时,这个操作符的作用不大,instanceof会返回一个布尔变量。
关于值类型和引用类型,以及instanceof,在第四章第一节会详述。
va s = "aaa";
var b =true;
var i = 22;
var u;
var n = null;
var o = new Object();
alert(typeof s);//string
alert(typeof i);//number
alert(typeof b);//boolean
alert(typeof u);//undefined
alert(typeof n);//Object
alert(typeof o);//Object
alert(person instanceof Object);//true
alert(person instanceof Array);//true or false
alert(person instanceof RegExp);//true or false
五、3.4.5 Number 类型
这一节坑有点大,单独做了一篇笔记:js Number parseInt parseFloat
六、3.4.6 字符串插值${}
模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。技术上讲,模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,只不过求值后得到的是字符串。模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。
字符串插值通过在${}
中使用一个 JavaScript 表达式实现:
let value = 5;
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
所有插入的值都会使用 toString()强制转型为字符串,而且任何 JavaScript 表达式都可以用于插值。嵌套的模板字符串无须转义:
console.log(`Hello, ${ `World` }!`); // Hello, World!
将表达式转换为字符串时会调用 toString():
let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!
在插值表达式中可以调用函数和方法:
function capitalize(word) {
return `${ word[0].toUpperCase() }${ word.slice(1) }`;
}
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!
此外,模板也可以插入自己之前的值:
let value = '';
function append() {
value = `${value}abc`
console.log(value);
}
append(); // abc
append(); // abcabc
append(); // abcabcabc
七、3.4.7节 Symbol(符号)是 ECMAScript 6 新增的数据类型
这里原书上来就讲细节,我都没搞懂Symbol是干嘛用的。可以先参考「每日一题」JS 中的 Symbol 是什么?
1.用处
我们在做一个游戏程序,用户需要选择角色的种族。
var race = {
protoss: 'protoss', // 神族
terran: 'terran', // 人族
zerg: 'zerg' // 虫族
}
function createRole(type){
if(type === race.protoss){创建神族角色}
else if(type === race.terran){创建人族角色}
else if(type === race.zerg){创建虫族角色}
}
那么用户选择种族后,就需要调用 createRole 来创建角色:
// 传入字符串
createRole('zerg')
// 或者传入变量
createRole(race.zerg)
一般传入字符串被认为是不好的做法,所以使用 createRole(race.zerg) 的更多。如果使用 createRole(race.zerg),那么聪明的读者会发现一个问题:race.protoss、race.terran、race.zerg 的值为多少并不重要。改为如下写法,对 createRole(race.zerg) 毫无影响:
var race = {
protoss: 'askdjaslkfjas;lfkjas;flkj', // 神族
terran: ';lkfalksjfl;askjfsfal;skfj', // 人族
zerg: 'qwieqwoirqwoiruoiwqoisrqwroiu' // 虫族
}
也就是说:race.zerg 的值是多少无所谓,只要它的值跟 race.protoss 和 race.terran 的值不一样就行。Symbol 的用途就是如此:Symbol 可以创建一个独一无二的值(但并不是字符串)。用 Symbol 来改写上面的 race:
var race = {
protoss: Symbol(),
terran: Symbol(),
zerg: Symbol()
}
race.protoss !== race.terran // true
race.protoss !== race.zerg // true
你也可以给每个 Symbol 起一个名字:
var race = {
protoss: Symbol('protoss'),
terran: Symbol('terran'),
zerg: Symbol('zerg')
}
不过这个名字跟 Symbol 的值并没有关系,你可以认为这个名字就是个注释。如下代码可以证明 Symbol 的名字与值无关:
var a1 = Symbol('a')
var a2 = Symbol('a')
a1 !== a2 // true
如果你觉得我说得还是太复杂了,看不懂,你可以记一句话:Symbol 生成一个全局唯一的值。
八、3.5.8 相等操作符
最早的ECMAScript在比较相等前,会先将对象转换成相似的类型,这就是==
- 如果有一个操作数是Boolean类型,会转化为数值0和1,比如false==0//true
- 如果一个是字符串,另一个是数字,则将字符串转化为数值,比如“5”==5//true
- 如果一个是对象,另一个不是,会调用对象的valueOf方法,得到基本类型
- 两个对象间的比较,比较引用地址
相等运算符隐藏的类型转换,会带来一些违反直觉的结果。
'' == '0' // false
0 == '' // true
0 == '0' // true
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
' \t\r\n ' == 0 // true
后来,有人提出这种比较的转换是否合理,于是ECMAScript提出一种不转换类型直接比较的===。"==="叫做严格运算符,"=="叫做相等运算符。
严格运算符的运算规则如下,
- (1)不同类型值
如果两个值的类型不同,直接返回false。 - (2)同一类的原始类型值
同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true,值不同就返回false。 - (3)同一类的复合类型值
两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象。
"55"==55//true
"55"===55//false
null==undefined//true
null===undefined//false
1.参考Javascript 中 == 和 === 区别是什么?
以工程标准衡量,“==”带来的便利性抵不上其带来的成本。举个简单的例子,团队协作中你肯定需要读别人的代码。而当你看到“==”时,要判断清楚作者的代码意图是确实需要转型,还是无所谓要不要转型只是随手写了,还是不应该转型但是写错了……所花费的脑力和时间比明确的“===”(加上可能需要的明确转型)要多得多。要记得团队中的每个人(包括原作者自己)都需要付出这理解和维护成本。
function fix(n) {
if (n == 0) return x + 1;
return x + 2;
}
如果输入n为字符串值"0"的话,恭喜你,你的程序爆炸啦! 你将会得到字符串"01"作为返回值,而不是你想要的数字1。
所以一句话概括:没有类型限制,类型转换的后果将是不可预料的。而且你写的程序很大的话,你可能在这上面浪费好几个小时找 bug。所以在自己需求明确的情况下,为什么不写===来避免可能的 bug 呢?
九、3.6节 语句
1.for
let count = 10;
for (let i = 0; i < count; i++) {
console.log(i);
}
2.for in
for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,下面是一个例子:
for (const propName in window) {
document.write(propName);
}
这个例子使用 for-in 循环显示了 BOM 对象 window 的所有属性。每次执行循环,都会给变量propName 赋予一个 window 对象的属性作为值,直到 window 的所有属性都被枚举一遍。与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const。
ECMAScript 中对象的属性是无序的,因此 for-in 语句不能保证返回对象属性的顺序。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异。
如果 for-in 循环要迭代的变量是 null 或 undefined,则不执行循环体。
3.for-of
for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素,下面是示例:
for (const el of [2,4,6,8]) {
document.write(el);
}
在这个例子中,我们使用 for-of 语句显示了一个包含 4 个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const。
for-of 循环会按照可迭代对象的 next()方法产生值的顺序迭代元素。关于可迭代对象,本书将在第 7 章详细介绍。如果尝试迭代的变量不支持迭代,则 for-of 语句会抛出错误。
注意 ES2018 对 for-of 语句进行了扩展,增加了 for-await-of 循环,以支持生成期约(promise)的异步可迭代对象。相关内容将在附录 A 介绍。
4.参考js中for in与for of之间的差异
推荐在循环对象属性的时候,使用for…in,在遍历数组的时候的时候使用for…of。for…in循环出的是key,for…of循环出的是value。
注意,for…of是ES6新引入的特性。修复了ES5引入的for…in的不足。假设我们往数组添加一个属性name:aArray.name = ‘demo’
,再分别查看上面写的两个循环:
for(let index in aArray){
console.log(`${aArray[index]}`); //Notice!!aArray.name也被循环出来了
}
for(var value of aArray){
console.log(value);
}
所以说,作用于数组的for-in循环除了遍历数组元素以外,还会遍历自定义属性。for…of循环不会循环对象的key,只会循环出数组的value,因此for…of不能循环遍历普通对象,对普通对象的属性遍历推荐使用for…in