本章概要
- 块作用域构造
- let 声明
- const 声明
- 全局块作用域绑定
- 模板字面量
- 多行字符串
- 字符串占位符
- 默认参数
- rest 参数
- 展开运算符
3.1 块作用域构造
块级声明用于声明在指定块的作用域之外无法访问的变量。块级作用域存在于:
- 函数内部
- 块中(字符“{”,“}”之前的区域)
3.1.1 let 声明
在函数作用域或全局作用域中通过关键字 var 声明变量,无论在哪里声明,都会被当成在当前作用域顶部声明的变量,这就是 JavaScript 的变量提升机制。
//函数内部
function changeState(flag){
if(flag){
var color = "red";
}else{
//此处可以访问变量 color ,其值为 undefined
console.log(color);
return null;
}
}
changeState(false);
//块中
{
var a = 1;
}
//此处可以访问变量 a,输出 a = 1
console.log("a = " + a);
//for 循环中
for(var i = 1;i<10;i++){
}
//此处不能访问变量 i,输出 1 = 10
console.log("i = " + i);
这种变量提升机制在开发时会造成很多的困扰,ES6 引入了 let 声明,用法与 var 相同,不过用 let 声明的变量不会被提升,可以将变量的作用域限制在当前代码块中。
将上述代码中的变量用 let 进行声明。如下:
//函数内部
function changeState(flag){
if(flag){
let color = "red";
}else{
//此处不可以访问变量 color ,报错:color is not defined
console.log(color);
return null;
}
}
changeState(false);
//块中
{
let a = 1;
}
//此处不可以访问变量 a ,报错:a is not defined
console.log("a = " + a);
//for 循环中
for(let i = 1;i<10;i++){
}
//此处不可以访问变量 i ,报错:i is not defined
console.log("i = " + i);
使用 let 声明变量,还可以防止变量的重复声明。例如,在某个作用域下已经存在某个标识符,此时再使用 let 关键字声明它,就会抛出错误。如下
var index = 0;
var index = 10;
// 报错:Identifier 'index' has already been declared
let index = 100;
在同一作用域下,不能使用 let 重复声明已经存在的标识符,但如果在不同的作用域下则是可以的。
3.1.2 const 声明
ES6 还提供了 const 关键字,用于声明常量。每个通过 const 关键字声明的常量必须在声明的同事进行初始化。如下:
const m = 10;
//错误
const n;
n = 20;
与 let 声明类似,在同一作用域下用 const 声明已经存在的标识符也会导致语法错误,无论该标识符是使用 var ,还是 let 声明的。
如果使用 const 声明对象,对象本身的绑定不能修改,但对象的属性和值是可以修改的。如下:
const person = {
name:"zhangsan"
};
person.name = "lisi";
person.age = 20;
//错误,报错:Assignment to constant variable
person = {
name:"wangwu"
}
3.1.3 全局块作用域绑定
在全局作用域中使用 var 声明的变量或对象,将作为浏览器环境中的 Windows 对象的属性,这意味着使用 var 很可能会无意中覆盖一个已经存在的全局属性。如下:
var greeting = "Welcome";
// 打印 Welcome
console.log(window.greeting);
// 打印 function Screen(){[native code]}
console.log(window.Screen);
var Screen = "aaa";
// 打印 aaa
console.log(window.Screen);
greeting 被定义为一个全局变量,并立即成为 window 对象的属性。定义的全局变量 Screen,则覆盖了 window 对象中原有的 Screen 属性。
如果在全局作用域下使用 let 或 const,则会在全局作用域下创建一个新的绑定,该绑定不会成为 window 对象的属性。如下:
let greeting = "Welcome";
// undefined
console.log(window.greeting);
const Screen = "aaa";
// 打印 false
console.log(window.Screen == Screen);
综上所述,如果不想为全局对象 windows 创建属性,或者为了避免覆盖 windows 对象的属性,则应该使用 let 和 const 声明变量和常量。
3.2 模板字面量
ES6 引入了 模板字面量(Template Literals),对字符串的操作进行了增强:
- 多行字符串:真正的多行字符串
- 字符串占位符:可以将变量或 JavaScript 表达式嵌入占位符中并将其作为字符串的一部分输出到结果中
3.2.1 多行字符串
模板字面量的基础语法就是反文号“`”替换字符串的单、双引号。如下:
let message = `hello`;
这句代码使用模板字面量语法创建了一个字符串,并赋值给 message 变量,这时变量的值与一个普通的字符串并无差异。
如果想要在字符串中使用反引号,那么用反斜杠“\”将它转义即可,如下:
let message = `hello \`world\``;
在模板字面量中,不需要转义单、双引号。
在 ES5 中,如果一个字符串字面量要分为多行书写,那么可以采用两种方式来实现:在一行结尾的时候添加反斜杠“\” 表示承接下一行的代码,或者使用“+”来拼接字符串,如下:
let a = "hello \
world";
let b = "hello"
+ "world";
这两种实现方式,前者是利用 JavaScript 的语法 Bug 来实现,后者是利用字符串的拼接操作来实现。当把字符串 a 和 b 打印时,这两个字符串均未跨行显示,前者使用反斜杠只是代表行的延续,并未真正插入新的一行。如果要输出新的一行,需要手动加入换行符。如下:
let a = "hello \n \
world";
let b = "hello"
+"\n"
+ "world";
在 ES6 中,使用模板字面量语法,可以方便地实现多行字符串的创建。如果需要在字符串中添加新的一行,只需要在代码中直接换行即可。如下:
let m = `ni
hao`;
注意:在反引号中的所有空白字符(包括但不限于空格、换行、制表符)都属于字符串的一部分。
3.2.2 字符串占位符
在一个模板字面量中,可以将 JavaScript 变量或任何合法的 JavaScript 表达式嵌入占位符并将其作为字符串的一部分输出到结果中。占位符由一个左侧的“${”和右侧的“}”符号组成,中间可以包含变量或JavaScript表达式。如下:
let name = "zhangsan"
let message = `ni hao,${name}`;
console.log(message);
let a = 5;
let b = 10;
let num = `jie guo : ${a * b}`;
console.log(num)
模板字面量本身也是 JavaScript 表达式,因此也可以在一个模板字面量中嵌入另一个模板字面量。如下:
let name = "zhangsan"
let message = `ni hao , ${ `wo shi ${name}`}`;
console.log(message)
3.3 默认参数
在 ES5 中,没有提供直接在函数的参数列表中指定参数默认值的语法,要想为函数参数指定默认值,只能通过下面方式实现:
function makeRedirect(url,timeout){
url = url || "/home";
timeout = timeout || 2000;
// 函数其余部分
}
在这个示例中,url 和 timeout 是可选参数,如果不传入对应的参数值,他们也将被赋予一个默认值。但是这种模式设置函数的默认值有一个缺陷,如果形参 timeout 传入值 0 ,及时这个值是合法的,也会被视为一个假值,并最终将 timeout 设置为 2000。
在这种情况下,更安全的做法是通过 typeof 检查参数类型,如下:
function makeRedirect(url,timeout){
url = (typeof url != "undefined") ? url : "/home";
timeout = (typeof timeout != "undefined") ? timeout : 2000;
// 函数其余部分
}
尽管这种方式更加安全,但需要额外的代码来执行这种非常基础的操作。在 ES6 中,简化了为形参提供默认值的过程,可以直接在参数列表中为形参指定默认值。如下:
function makeRedirect(url = "/home",timeout = 2000){
// 函数其余部分
}
如果调用 makeRedirect() ,则使用参数 url 和 timeout 的默认值;如果调用 makeRedirect(“/login”) ,则使用参数timeout的默认值;如果调用 makeRedirect() 时传入两个参数,则不使用默认值。
此外,在 ES6 中声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后还可以继续声明无默认值的参数,如下:
function makeRedirect(url = "/home",timeout = 2000,callback){
// 函数其余部分
}
在这种情况下,只有在没有为 url 和 timeout 传值,或者主动为它们传入 undefined 时才会使用它们的默认值,如下:
//使用 url 和 timeout 的默认值
makeRedirect();
makeRedirect(undefined,undefined,function(){});
//使用 timeout 的默认值
makeRedirect("/login");
//不适用 timeout 的默认值
makeRedirect("/login",null,function(){});
为一个具有默认值的参数传值 null 是合法的,所以,最后一次调用 makeRedirect() 时,将不会使用 timeout 的默认值,其值最终为 null。
3.4 rest 参数
JavaScript 函数一个特别的地方是,无论在函数中声明了多少形参,都可以传入任意数量的参数,在函数内部可以通过 arguments 对象接收传入的参数,如下:
function calculate(op) {
if (op === "+") {
let result = 0;
for (let i = 1; i < arguments.length; i++) {
result += arguments[i];
}
return result;
} else if (op === "*") {
let result = 1;
for (let i = 1; i < arguments.length; i++) {
result *= arguments[i];
}
return result;
}
}
calculate() 函数根据传入的操作符的不同而执行不同的计算,计算的数据可以是任意多个,因此在函数声明时无法明确地定义要传入的所有参数。但是,可以通过 arguments 对象解决任意数量参数传入的问题。
不过这种方式也有一些不足的地方:首先,调用者需要知道该函数可以接受任意数量的参数,单从函数声明的参数列表是看不出来的;其次,因为第一个参数是命名参数且已被使用,因此遍历 arguments 对象时,索引要从1开始而不是0。
ES6 引入了 rest 参数,在函数的命名参数前添加3个点,就表明这是一个rest参数,用于获取函数的多余参数。rest参数是一个数组,包含自它之后传入的所有参数,通过这个数组名就可以逐一访问里面的参数。
使用rest 参数重写上例中的 calculate() 函数,如下:
function calculate(op, ...data){
if(op === "+"){
let result = 0;
for(let i = 0; i < data.length; i++){
result += data[i];
}
return result;
} else if(op === "*"){
let result = 1;
for(let i = 0; i < data.length; i++){
result *= data[i];
}
return result;
}
}
rest 参数包含的是 op 之后传入的所有参数(arguments 对象包含的是所有传入的参数,包括 op)。可以看到,使用 rest 参数,函数可以处理的参数数量一目了然,代码则更加清晰。
需要注意的是,每个函数最多只能声明一个 rest 参数,并且它只能是最后一个参数。
3.5 展开运算符
展开预算符在语法上与 rest 参数相似,也是3个点,它可以将一个数组转换为各个独立的参数,也可用于取出对象的所有可遍历属性,而 rest 参数是指定多个独立的参数,并通过整合后的数组来访问,如下:
function sum(a, b, c){
return a + b + c;
}
let arr = [1, 2, 3];
sum(...arr);
上述代码使用展开运算符提取数组 arr 中的各个值并传入 sum() 函数中。
展开运算符可以用来复制数组,如下:
let arr1 = [1, 2, 3];
// arr2与arr1是同一个数组对象
let arr2 = arr1;
// arr3与arr1不是同一个数组对象
let arr3 = [...arr1];
arr1[0] = 4;
// arr2 中的元素同事被改变,输出4
console.log(arr2[0]);
// 输出1
console.log(arr3[0]);
从上述代码可以看到,在需要复制一个新的数组对象时,可以使用展开运算符便捷地实现。
展开运算符也可以用来合并数组,如下:
let arr1 = ['a'];
let arr2 = ['b', 'c'];
let arr3 = ['d', 'e'];
console.log([...arr1, ...arr2, ...arr3]); //[ 'a', 'b', 'c', 'd', 'e' ]
展开运算符还可以用于取出对象的所有可遍历属性,复制到当前对象中,如下:
let book = {
tille: "你好",
price: 98
}
let bookDetail = {...book, desc: "world"}
//{ tille: '你好', price: 98, desc: 'world' }
console.log(bookDetail);