3 语言基础

1 语法

1.1 区分大小写

ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。类似地,typeof 不能作为函数名,因为它是一个关键字,但 Typeof 是一个完全有效的函数名。

1.2 标识符

所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成:

  • 第一个字符必须是一个字母、下划线_或美元符号$
  • 剩下的其他字符可以是字母、下划线、美元符号或数字

标识符中的字母可以是扩展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符,如 ÀÆ(但不推荐使用)。

按照惯例,ECMAScript 标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写,如:

firstSecond 
myCar 
doSomethingImportant 

虽然这种写法并不是强制性的,但因为这种形式跟 ECMAScript 内置函数和对象的命名方式一致,所以算是最佳实践。

1.3 注释

ECMAScript 采用 C 语言风格的注释,包括单行注释和块注释。单行注释以两个斜杠字符开头,如:

// 单行注释

块注释以一个斜杠和一个星号(/*)开头,以它们的反向组合(*/)结尾,如:

/* 这是多行
注释 */ 
1.4 严格模式

ECMAScript 5 introduced the concept of strict mode. Strict mode is a different parsing and execution model for JavaScript, where some of the erratic behavior of ECMAScript 3 is addressed and errors are thrown for unsafe activities. To enable strict mode for an entire script, include the following at the top:

"use strict";

Although this may look like a string that isn’t assigned to a variable, this is a pragma that tells supporting JavaScript engines to change into strict mode. The syntax was chosen specifically so as not to
break ECMAScript 3 syntax.

You may also specify just a function to execute in strict mode by including the pragma at the top of the function body:

function doSomething() { 
 	"use strict"; 
 	// 函数体 
}
1.5 语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子所示:

let sum = a + b // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐

即使语句末尾的分号不是必需的,也应该加上。记着加分号有助于防止省略造成的问题,比如可以避免输入内容不完整。此外,加分号也便于开发者通过删除空行来压缩代码(如果没有结尾的分号,只删除空行,则会导致语法错误)。加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。

多条语句可以合并到一个 C 语言风格的代码块中。代码块由一个左花括号({)标识开始,一个右花括号(})标识结束:

if (test) { 
 	test = false; 
 	console.log(test); 
} 

if 之类的控制语句只在执行多条语句时要求必须有代码块。不过,最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句,如下例所示:

// 有效,但容易导致错误,应该避免
if (test) 
 	console.log(test);
 	 
// 推荐
if (test) { 
 	console.log(test); 
} 

在控制语句中使用代码块可以让内容更清晰,在需要修改代码时也可以减少出错的可能性。

2 关键字与保留字

待补充 48

3 变量

ECMAScript variables are loosely typed, meaning that a variable can hold any type of data. Every variable is simply a named placeholder for a value. There are three keywords that can be used to declare a variable: var, which is available in all ECMAScript versions, and const and let, which were introduced in ECMAScript 6.

3.1 The ’var’ Keyword

To define a variable, use the var operator followed by the variable name (an identifier, as described earlier), like this:

var message;

This code defines a variable named message that can be used to hold any value. (Without initialization, it holds the special value undefined) ECMAScript implements variable initialization, so it’s possible to define the variable and set its value at the same time, as in this example:

var message = "hi";

Here, message is defined to hold a string value of "hi". Doing this initialization doesn’t mark the variable as being a string type; it is simply the assignment of a value to the variable. It is still possible to not only change the value stored in the variable but also change the type of value, such as this:

var message = "hi"; 
message = 100; // 合法,但不推荐
3.1.1 var Declaration Scope

It’s important to note that using the var operator to define a variable makes it local to the function scope in which it was defined. For example, defining a variable inside of a function using var means that the variable is destroyed as soon as the function exits, as shown here:

function test() { 
 	var message = "hi"; // 局部变量
}
 
test(); 
console.log(message); // 出错!

Here, the message variable is defined within a function using var. The function is called test(), which creates the variable and assigns its value. Immediately after that, the variable is destroyed so the last line in this example causes an error. It is, however, possible to define a variable globally by simply omitting the var operator as follows:

function test() { 
 	message = "hi"; // 全局变量
} 

test(); 
console.log(message); // "hi" 

By removing the var operator from the example, the message variable becomes global. As soon as the
function test() is called, the variable is defined and becomes accessible outside of the function once
it has been executed.

NOTE Although it’s possible to define global variables by omitting the var operator, this approach is not recommended. Global variables defined locally are
hard to maintain and cause confusion because it’s not immediately apparent if the omission of var was intentional. Strict mode throws a ReferenceError when an undeclared variable is assigned a value.

If you need to define more than one variable, you can do it using a single statement, separating each variable (and optional initialization) with a comma like this:

var message = "hi", found = false, age = 29; 

When you are running in strict mode, you cannot define variables named eval or arguments. Doing so results in a syntax error.

3.1.2 var Declaration Hoisting

When using var, the following is possible because variables declared using that keyword are hoisted to the top of the function scope:

function foo() { 
 	console.log(age); 
 	var age = 26; 
} 

foo(); // undefined 

This does not throw an error because the ECMAScript runtime technically treats it like this:

function foo() { 
 	var age; 
 	console.log(age); 
 	age = 26; 
} 

foo(); // undefined 

This is “hoisting,” where the interpreter pulls all variable declarations to the top of its scope. It also allows you to use redundant var declarations without penalty:

function foo() { 
 	var age = 16; 
 	var age = 26; 
 	var age = 36; 
 	console.log(age); 
} 

foo(); // 36 
3.2 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 没有定义

在这里,age 变量之所以不能在 if 块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于 var 的作用域限制同样也适用于 let。

let 也不允许同一个块作用域中出现冗余声明。这样会导致报错:

var name; 
var name; 
let age; 
let age; // SyntaxError;标识符 age 已经声明过了

JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明:

var name = 'Nicholas'; 
console.log(name); // 'Nicholas' 

if (true) { 
 	var name = 'Matt'; 
 	console.log(name); // 'Matt' 
} 

let age = 30; 
console.log(age); // 30 
if (true) { 
 	let age = 26; 
 	console.log(age); // 26 
}

对声明冗余报错不会因混用 let 和 var 而受影响。这两个关键字声明的并不是不同类型的变量,它们只是指出变量在相关作用域如何存在。

var name; 
let name; // SyntaxError 
let age; 
var age; // SyntaxError
3.2.1 暂时性死区

let 与 var 的另一个重要的区别是 let 声明的变量不会在作用域中被提升。

// name 会被提升
console.log(name); // undefined 
var name = 'Matt'; 

// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26; 

在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError。

3.2.2 全局声明

与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会):

var name = 'Matt'; 
console.log(window.name); // 'Matt' 

let age = 26; 
console.log(window.age); // undefined

不过,let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了避免 SyntaxError,必须确保页面不会重复声明同一个变量。

3.3.3 条件声明

在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。而 let 的作用域是块,我们需要检查前面是否已经使用过 let 同名变量,以便我们在没有声明的情况下声明它。

<script> 
	var name = 'Nicholas'; 
 	let age = 26; 
</script>
 
<script> 
 	// 假设脚本不确定页面中是否已经声明了同名变量
 	// 那它可以假设还没有声明过
 	var name = 'Matt'; 
 	// 这里没问题,因为可以被作为一个提升声明来处理
 	// 不需要检查之前是否声明过同名变量
 	
 	let age = 36; 
 	// 如果 age 之前声明过,这里会报错
</script>

使用 try/catch 语句或 typeof 操作符也不能解决,因为条件块中 let 声明的作用域仅限于该块。

<script> 
 	let name = 'Nicholas'; 
 	let age = 36; 
</script>
 
<script> 
 	// 假设脚本不确定页面中是否已经声明了同名变量
 	// 那它可以假设还没有声明过
 	
 	if (typeof name === 'undefined') { 
 		let name; 
 	} 
 	// name 被限制在 if {} 块的作用域内
 	// 因此这个赋值形同全局赋值
 	name = 'Matt'; 
 	
 	try { 
 		console.log(age); // 如果 age 没有声明过,则会报错
 	} 
 	catch(error) { 
 		let age; 
 	} 
 	// age 被限制在 catch {}块的作用域内
 	// 因此这个赋值形同全局赋值
 	age = 26; 
</script> 

为此,对于 let 这个新的 ES6 声明关键字,不能依赖条件声明模式。

不能使用 let 进行条件式声明是件好事,因为条件声明是一种反模式,它让程序变得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式。

3.3.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 循环。

3.3 const声明

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

const age = 26; 
age = 36; // TypeError: 给常量赋值

// const 也不允许重复声明
const name = 'Matt'; 
const name = 'Nicholas'; // SyntaxError

// const 声明的作用域也是块
const name = 'Matt'; 
if (true) { 
 const name = 'Nicholas'; 
} 
console.log(name); // Matt 

const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反 const 的限制。

const person = {}; 
person.name = 'Matt'; // ok 

JavaScript 引擎会为 for 循环中的 let 声明分别创建独立的变量实例,虽然 const 变量跟 let 变量很相似,但是不能用 const 来声明迭代变量(因为迭代变量会自增):

for (const i = 0; i < 10; ++i) {} // TypeError:给常量赋值

不过,如果你只想用 const 声明一个不会被修改的 for 循环变量,那也是可以的。也就是说,每次迭代只是创建一个新变量。这对 for-of 和 for-in 循环特别有意义:

let i = 0; 
for (const j = 7; i < 5; ++i) { 
 	console.log(j); 
} 
// 7, 7, 7, 7, 7 

for (const key in {a: 1, b: 2}) { 
 	console.log(key); 
} 
// a, b 

for (const value of [1,2,3,4,5]) { 
 console.log(value); 
} 
// 1, 2, 3, 4, 5

4 DATA TYPES

There are six simple data types (also called primitive types) in ECMAScript: Undefined, Null, Boolean, Number, String, and Symbol. Symbol was newly introduced in ECMAScript 6. There is also one complex data type called Object, which is an unordered list of name–value pairs. Because there is no way to define your own data types in ECMAScript, all values can be represented as one of these seven. Having only seven data types may seem like too few to fully represent data; however,
ECMAScript’s data types have dynamic aspects that make each single data type behave like several.

4.1 The typeof Operator

Because ECMAScript is loosely typed, there needs to be a way to determine the data type of a given variable. The typeof operator provides that information. Using the typeof operator on a value returns one of the following strings:

  • "undefined" if the value is undefined
  • "boolean" if the value is a Boolean
  • "string" if the value is a string
  • "number" if the value is a number
  • "object" if the value is an object (other than a function) or null
  • "function" if the value is a function
  • "symbol" if the value is a Symbol

The typeof operator is called like this:

let message = "some string"; 

console.log(typeof message); 	// "string" 
console.log(typeof(message)); 	// "string" 
console.log(typeof 95); 		// "number" 

In this example, both a variable (message) and a numeric literal are passed into the typeof operator. Note that because typeof is an operator and not a function, no parentheses are required (although they can be used).

Be aware there are a few cases where typeof seemingly returns a confusing but technically correct value. Calling typeof null returns a value of "object", as the special value null is considered to be an empty object reference.

NOTE Technically, functions are considered objects in ECMAScript and don’t represent another data type. However, they do have some special properties, which necessitates differentiating between functions and other objects via the typeof operator.

4.6 String类型

String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列。字符串可以使用双引号"、单引号'、反引号`表示,因此下面的代码都是合法的:

let firstName = "John"; 
let lastName = 'Jacob'; 
let lastName = `Jingleheimerschmidt`;

跟某些语言中使用不同的引号会改变对字符串的解释方式不同,ECMAScript 语法中表示字符串的引号没有区别。不过要注意的是,以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。比如,下面的写法会导致语法错误:

let firstName = 'Nicholas"; // 语法错误:开头和结尾的引号必须是同一种
4.6.1 字符字面量

字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:

待补充 63

4.6.2 字符串的特点

ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如下所示:

let lang = "Java"; 
lang = lang + "Script";

这里,变量 lang 一开始包含字符串"Java"。紧接着,lang 被重新定义为包含"Java"和"Script"的组合,也就是"JavaScript"。整个过程首先会分配一个足够容纳 10 个字符的空间,然后填充上"Java"和"Script"。最后销毁原始的字符串"Java"和字符串"Script",因为这两个字符串都没有用了。所有处理都是在后台发生的,而这也是一些早期的浏览器(如 Firefox 1.0 之前的版本和 IE6.0)在拼接字符串时非常慢的原因。这些浏览器在后来的版本中都有针对性地解决了这个问题。

4.6 3 转换为字符串

有两种方式把一个值转换为字符串。首先是使用几乎所有值都有的 toString()方法。这个方法唯一的用途就是返回当前值的字符串等价物。比如:

let age = 11; 
let ageAsString = age.toString(); // 字符串"11" 
let found = true; 
let foundAsString = found.toString(); // 字符串"true" 

toString()方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有 toString()方法,该方法只是简单地返回自身的一个副本。)null 和 undefined 值没有 toString()方法。

多数情况下,toString()不接收任何参数。不过,在对数值调用这个方法时,toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,toString()返回数值的十进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示,比如:

let num = 10; 
console.log(num.toString()); // "10" 
console.log(num.toString(2)); // "1010" 
console.log(num.toString(8)); // "12" 
console.log(num.toString(10)); // "10" 
console.log(num.toString(16)); // "a" 

这个例子展示了传入底数参数时,toString()输出的字符串值也会随之改变。数值 10 可以输出为任意数值格式。注意,默认情况下(不传参数)的输出与传入参数 10 得到的结果相同。

如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。

  • 如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
  • 如果值是 null,返回"null"。
  • 如果值是 undefined,返回"undefined"。

注意 用加号操作符给一个值加上一个空字符串""也可以将其转换为字符串(加号操作符本章后面会介绍)

4.6.4 Template Literals

New in ECMAScript 6 is the capability to define strings using template literals. Unlike their single
and double quoted counterparts, template literals respect new line characters, and can be defined
spanning multiple lines:

let myMultiLineString = 'first line\nsecond line'; 
let myMultiLineTemplateLiteral = `first line 
second line`; 

console.log(myMultiLineString); 
// first line 
// second line" 

console.log(myMultiLineTemplateLiteral); 
// first line
// second line 

console.log(myMultiLineString === myMultiLinetemplateLiteral); 	// true 

As the name suggests, template literals are especially useful when defining templates, such as HTML:

let pageHTML = ` 
<div> 
 	<a href="#"> 
 		<span>Jake</span> 
 	</a> 
</div>`; 

Because template literals will exactly match the whitespace inside the backticks, special care will need to be applied when defining them. A correctly formatted template string may appear to have improper indentation:

// This template literal has 25 spaces following the line return character
let myTemplateLiteral = `first line 
 						 second line`; 
console.log(myTemplateLiteral.length); // 47
 
// This template literal begins with a line return character
let secondTemplateLiteral = ` 
first line 
second line`; 
console.log(secondTemplateLiteral[0] === '\n'); // true
 
// This template literal has no unexpected whitespace characters
let thirdTemplateLiteral = `first line 
second line`; 
console.log(thirdTemplateLiteral); 
// first line 
// second line
4.6.5 Interpolation

One of the most useful features of template literals is their support for interpolation, which allows you to insert values at one or more places inside a single unbroken definition. Technically, template literals aren’t strings, they are special JavaScript syntactical expressions that evaluate into strings. Template literals are evaluated immediately when they are defined and converted into a string instance, and any interpolated variables will be drawn from its immediate scope.

This can be accomplished using a JavaScript expression inside ${}:

let value = 5; 
let exponent = 'second'; 

// Formerly, interpolation was accomplished as follows:
let interpolatedString = 
 	value + ' to the ' + exponent + ' power is ' + (value * value); 
 
// The same thing accomplished with template literals:
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 

The value being interpolated will eventually be coerced into a string using toString(), but any JavaScript expression can safely be interpolated. Nesting template strings is safe with no escaping required:

console.log(`Hello, ${ `World` }!`); 	// Hello, World! 

toString() is invoked to coerce expression result into string:

let foo = { toString: () => 'World' }; 
console.log(`Hello, ${ foo }!`); // Hello, World! 

Invoking functions and methods inside interpolated expressions is allowed:

function capitalize(word) { 
 	return `${ word[0].toUpperCase() }${ word.slice(1) }`; 
} 
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World! 

Additionally, templates can safely interpolate their previous value:

let value = ''; 
function append() { 
 	value = `${value}abc` 
 	console.log(value); 
} 

append(); // abc 
append(); // abcabc 
append(); // abcabcabc 
4.6.6 模板字面量标签函数

待补充 67

4.6.1 字符字面量(逃逸字符)

字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:

字面量含义
字面量含义
\n换行
\t制表
\b退格
\r回车
\f换页
\\反斜杠( \ )
\ ’单引号('),在字符串以单引号标示时使用,例如’He said, \ 'hey.\ ’ ’
\ "双引号("),在字符串以双引号标示时使用,例如"He said, \ "hey.\ " "
\ `反引号(`),在字符串以反引号标示时使用,例如`He said, \ `hey.\ ` `
\xnn以十六进制编码nn表示的字符(其中n是十六进制数字0~F),例如\x41 等于"A"
\unnn以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F),例如\u03a3 等于希腊字符"Σ"

这些字符字面量可以出现在字符串中的任意位置,且可以作为单个字符被解释:

let text = "This is the letter sigma: \u03a3."; 

在这个例子中,即使包含 6 个字符长的转义序列,变量 text 仍然是 28 个字符长。因为转义序列表示一个字符,所以只算一个字符。

字符串的长度可以通过其 length 属性获取:

console.log(text.length); // 28

这个属性返回字符串中 16 位字符的个数。

注意 如果字符串中包含双字节字符,那么length 属性返回的值可能不是准确的字符数。第 5 章将具体讨论如何解决这个问题。

4.7 Symbol 类型

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为Object API 提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。

4.7.1 符号的基本用法

符号需要使用 Symbol()函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回 symbol。

let sym = Symbol(); 
console.log(typeof sym); // symbol 

调用 Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:

let genericSymbol = Symbol(); 
let otherGenericSymbol = Symbol(); 

let fooSymbol = Symbol('foo'); 
let otherFooSymbol = Symbol('foo'); 

console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false 

符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建 Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

let genericSymbol = Symbol(); 
console.log(genericSymbol); // Symbol() 

let fooSymbol = Symbol('foo'); 
console.log(fooSymbol); // Symbol(foo); 

最重要的是,Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原始值的包装对象:

let myBoolean = new Boolean(); 
console.log(typeof myBoolean); // "object" 
let myString = new String(); 
console.log(typeof myString); // "object" 
let myNumber = new Number(); 
console.log(typeof myNumber); // "object" 
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor 

如果你确实想使用符号包装对象,可以借用 Object()函数:

let mySymbol = Symbol(); 
let myWrappedSymbol = Object(mySymbol); 
console.log(typeof myWrappedSymbol); // "object"
4.7.2 使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。

为此,需要使用 Symbol.for()方法:

let fooGlobalSymbol = Symbol.for('foo'); 
console.log(typeof fooGlobalSymbol); // symbol

Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号

console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同:

let localSymbol = Symbol('foo'); 
let globalSymbol = Symbol.for('foo'); 

console.log(localSymbol === globalSymbol); // false 

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

let emptyGlobalSymbol = Symbol.for(); 
console.log(emptyGlobalSymbol); // Symbol(undefined)

还可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。

// 创建全局符号
let s = Symbol.for('foo'); 
console.log(Symbol.keyFor(s)); // foo 

// 创建普通符号
let s2 = Symbol('bar'); 
console.log(Symbol.keyFor(s2)); // undefined 

如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:

Symbol.keyFor(123); // TypeError: 123 is not a symbol 
4.7.3 使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。

let s1 = Symbol('foo'), 
 	s2 = Symbol('bar'), 
 	s3 = Symbol('baz'), 
 	s4 = Symbol('qux'); 
 	
let o = { 
 	[s1]: 'foo val' 
}; 
// 这样也可以:o[s1] = 'foo val';
 
console.log(o); 
// {Symbol(foo): foo val} 

Object.defineProperty(o, s2, {value: 'bar val'});
 
console.log(o); 
// {Symbol(foo): foo val, Symbol(bar): bar val} 

Object.defineProperties(o, { 
 	[s3]: {value: 'baz val'}, 
 	[s4]: {value: 'qux val'} 
}); 

console.log(o); 
// {Symbol(foo): foo val, Symbol(bar): bar val, 
// Symbol(baz): baz val, Symbol(qux): qux val} 

类似于 Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型
的键:

let s1 = Symbol('foo'), 
 	s2 = Symbol('bar'); 
 	
let o = { 
 	[s1]: 'foo val', 
 	[s2]: 'bar val', 
 	baz: 'baz val', 
 	qux: 'qux val' 
}; 

console.log(Object.getOwnPropertySymbols(o)); 
// [Symbol(foo), Symbol(bar)] 

console.log(Object.getOwnPropertyNames(o)); 
// ["baz", "qux"] 

console.log(Object.getOwnPropertyDescriptors(o)); 
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} 

console.log(Reflect.ownKeys(o)); 
// ["baz", "qux", Symbol(foo), Symbol(bar)]

因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:

let o = { 
 	[Symbol('foo')]: 'foo val', 
 	[Symbol('bar')]: 'bar val' 
}; 

console.log(o); 
// {Symbol(foo): "foo val", Symbol(bar): "bar val"} 

let barSymbol = Object.getOwnPropertySymbols(o) 
 				.find((symbol) => symbol.toString().match(/bar/)); 
console.log(barSymbol); 
// Symbol(bar) 
4.7.4 常用内置符号

ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。

这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。

这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。

注意:在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。

4.7.8 Symbol.iterator

根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。

for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API的 Generator:

待补充,generator,生成器

class Foo { 
 	*[Symbol.iterator]() {} 
} 

let f = new Foo();
 
console.log(f[Symbol.iterator]()); 
// Generator {<suspended>} 

技术上,这个由 Symbol.iterator 函数生成的对象应该通过其 next()方法陆续返回值。可以通过显式地调用 next()方法返回,也可以隐式地通过生成器函数返回:

待补充 yield

class Emitter { 
 	constructor(max) { 
 		this.max = max; 
 		this.idx = 0; 
 	}
 	 
 	*[Symbol.iterator]() { 
 		while(this.idx < this.max) { 
 			yield this.idx++; 
 		} 
 	} 
} 

function count() { 
 	let emitter = new Emitter(5);
 	 
 	for (const x of emitter) { 
 		console.log(x); 
 	} 
} 

count(); 
// 0 
// 1 
// 2 
// 3 
// 4 
4.8 Object类型

ECMAScript 中的对象是一组数据和功能的集合。可以通过new 对象类型来创建对象,然后再给对象添加属性和方法:

let o = new Object(); 

ECMAScript 只要求在给构造函数提供参数时使用括号。如果没有参数,那么完全可以省略括号(不推荐):

let o = new Object; // 合法,但不推荐

Object 的实例本身并不是很有用,但理解与它相关的概念非常重要。ECMAScript 中的 Object 是派生其他对象的基类。Object 类型的所有属性和方法在派生的对象上同样存在。

每个 Object 实例都有如下属性和方法:

  • constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object() 函数
  • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty(“name”))或符号。
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(第八节介绍原型)
  • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。

因为在 ECMAScript 中 Object 是所有对象的基类,所以任何对象都有这些属性和方法。

注意 严格来讲,ECMA-262 中对象的行为不一定适合 JavaScript 中的其他对象。比如浏览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受 ECMA-262 约束,所以它们可能会也可能不会继承 Object。

5 操作符

5.8 相等操作符

在比较字符串、数值和布尔值是否相等时,过程很直观。但是在比较两个对象是否相等时,情形就比较复杂了。ECMAScript 中的相等和不相等操作符,原本在比较之前会执行类型转换,但很快就有人质疑这种转换是否应该发生。最终,ECMAScript提供了两组操作符。第一组是等于和不等于,它们在比较之前执行转换。第二组是全等和不全等,它们在比较之前不执行转换。

5.8.1 等于和不等于

ECMAScript 中的等于操作符用两个等于号 == 表示,如果操作数相等,则会返回 true。不等于操作符用叹号和等于号 != 表示,如果两个操作数不相等,则会返回 true。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。

在转换操作数的类型时,相等和不相等操作符遵循如下规则。

  • 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为 1。
  • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较。

在进行比较时,这两个操作符会遵循如下规则。

  • null 和 undefined 相等。
  • null 和 undefined 不能转换为其他类型的值再进行比较。
  • 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true。否则,两者不相等。

下表总结了一些特殊情况及比较的结果。
在这里插入图片描述

5.8.2 全等和不全等

全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操作符由 3 个等于号===表示,只有两个操作数在不转换的前提下相等才返回 true,比如:

let result1 = ("55" == 55); 	// true,转换后相等
let result2 = ("55" === 55); 	// false,不相等,因为数据类型不同

在这个例子中,第一个比较使用相等操作符,比较的是字符串"55"和数值 55。如前所述,因为字符串"55"会被转换为数值 55,然后再与数值 55 进行比较,所以返回 true。第二个比较使用全等操作符,因为没有转换,字符串和数值当然不能相等,所以返回 false。

不全等操作符用一个叹号和两个等于号!==表示,只有两个操作数在不转换的前提下不相等才返回 true。比如:

let result1 = ("55" != 55); // false,转换后相等
let result2 = ("55" !== 55); // true,不相等,因为数据类型不同

这一次,第一个比较使用不相等操作符,它会把字符串"55"转换为数值 55,跟第二个操作数进行比较。第二个比较使用不全等操作符,此时是字符串 55 与数值 55 进行比较。

另外,虽然 null == undefinedtrue(因为这两个值类似),但 null === undefinedfalse,因为它们不是相同的数据类型。

注意 由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。这样有助于在代码中保持数据类型的完整性。

5.9 条件操作符

条件操作符是 ECMAScript 中用途最为广泛的操作符之一,语法跟 Java 中一样:

variable = boolean_expression ? true_value : false_value;

上面的代码执行了条件赋值操作,即根据条件表达式 boolean_expression 的值决定将哪个值赋给变量 variable 。如果 boolean_expression 是 true ,则赋值 true_value ;如果boolean_expression 是 false,则赋值 false_value。比如:

let max = (num1 > num2) ? num1 : num2;

在这个例子中,max 将被赋予一个最大值。这个表达式的意思是,如果 num1 大于 num2(条件表达式为 true),则将 num1 赋给 max。否则,将 num2 赋给 max。

6 语句

ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中。语句通常使用一或多个关键字完成既定的任务。语句可以简单,也可以复杂。简单的如告诉函数退出,复杂的如列出一堆要重复执行的指令。

6.1 if 语句

if 语句是使用最频繁的语句之一,语法如下:

if (condition) statement1 else statement2

这里的条件(condition)可以是任何表达式,并且求值结果不一定是布尔值。ECMAScript 会自动调用 Boolean()函数将这个表达式的值转换为布尔值。如果条件求值为 true,则执行语句statement1;如果条件求值为 false,则执行语句 statement2。这里的语句可能是一行代码,也可能是一个代码块(即包含在一对花括号中的多行代码)。下面是一个例子:

if (i > 25) 
 	console.log("Greater than 25."); 				// 只有一行代码的语句
else { 
 	console.log("Less than or equal to 25."); 		// 一个语句块
}

推荐使用语句块。

可以像这样连续使用多个 if 语句:

if (condition1) statement1 else if (condition2) statement2 else statement3 

下面是一个例子:

if (i > 25) { 
 	console.log("Greater than 25."); 
} else if (i < 0) { 
 	console.log("Less than 0."); 
} else { 
 	console.log("Between 0 and 25, inclusive."); 
}
6.2 do-while语句

do-while 语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。do-while 的语法如下:

do { 
 	statement 
} while (expression); 

下面是一个例子:

let i = 0; 
do { 
 	i += 2; 
} while (i < 10);

在这个例子中,只要 i 小于 10,循环就会重复执行。i 从 0 开始,每次循环递增 2。

6.3 while 语句

while 语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while 循环体内的代码有可能不会执行。下面是 while 循环的语法:

while(expression) statement 

这是一个例子:

let i = 0; 
while (i < 10) { 
 	i += 2; 
} 

在这个例子中,变量 i 从 0 开始,每次循环递增 2。只要 i 小于 10,循环就会继续。

6.4 for 语句

for 语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式,语法如下:

for (initialization; expression; post-loop-expression) statement

下面是一个用例:

let count = 10; 
for (let i = 0; i < count; i++) { 
 	console.log(i); 
} 

以上代码在循环开始前定义了变量 i 的初始值为 0。然后求值条件表达式,如果求值结果为 true(i < count),则执行循环体。因此循环体也可能不会被执行。如果循环体被执行了,则循环后表达式也会执行,以便递增变量 i。for 循环跟下面的 while 循环是一样的:

let count = 10; 
let i = 0; 
while (i < count) { 
 	console.log(i); 
 	i++; 
} 

无法通过 while 循环实现的逻辑,同样也无法使用 for 循环实现。因此 for 循环只是将循环相关的代码封装在了一起而已。

在 for 循环的初始化代码中,其实是可以不使用变量声明关键字的。不过,初始化定义的迭代器变量在循环执行完成后几乎不可能再用到了。因此,最清晰的写法是使用 let 声明迭代器变量,这样就可以将这个变量的作用域限定在循环中。

初始化、条件表达式和循环后表达式都不是必需的。因此,下面这种写法可以创建一个无穷循环:

for (;;) { // 无穷循环
 	doSomething(); 
} 

如果只包含条件表达式,那么 for 循环实际上就变成了 while 循环:

let count = 10; 
let i = 0; 
for (; i < count; ) { 
 	console.log(i); 
 	i++; 
} 

这种多功能性使得 for 语句在这门语言中使用非常广泛。

6.5 for-in 语句

for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性(?),语法如下:

for (property in expression) statement 

下面是一个例子:

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,则不执行循环体。

6.6 for-of 语句

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

for (property of expression) statement 

下面是示例:

for (const el of [2,4,6,8]) { 
 	document.write(el); 
} 

在这个例子中,我们使用 for-of 语句显示了一个包含 4 个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const。

for-of 循环会按照可迭代对象的 next()方法产生值的顺序迭代元素。可迭代对象在第七节介绍。

如果尝试迭代的变量不支持迭代,则 for-of 语句会抛出错误。

注意 ES2018 对 for-of 语句进行了扩展,增加了 for-await-of 循环,以支持生成期约(promise)的异步可迭代对象。相关内容将在附录 A 介绍。

6.7 标签语句

标签语句用于给语句加标签,语法如下:

label: statement 

下面是一个例子:

start: for (let i = 0; i < count; i++) { 
 	console.log(i); 
} 

在这个例子中,start 是一个标签,可以在后面通过 break 或 continue 语句引用。标签语句的典型应用场景是嵌套循环。

6.8 break和continue语句

break 和 continue 语句为执行循环代码提供了更严格的控制手段。其中,break 语句用于立即退出循环,强制执行循环后的下一条语句。而 continue 语句也用于立即退出循环,但会再次从循环顶部开始执行。下面看一个例子:

let num = 0; 
for (let i = 1; i < 10; i++) { 
 	if (i % 5 == 0) { 
 		break;
  	} 
 	num++; 
} 

console.log(num); // 4

在上面的代码中,for 循环会将变量 i 由 1 递增到 10。而在循环体内,有一个 if 语句用于检查 i 能否被 5 整除(使用取模操作符)。如果是,则执行 break 语句,退出循环。变量 num 的初始值为 0,表示循环在退出前执行了多少次。当 break 语句执行后,下一行执行的代码是 console.log(num),显示 4。之所以循环执行了 4 次,是因为当 i 等于 5 时,break 语句会导致循环退出,该次循环不会执行递增 num 的代码。如果将 break 换成 continue,则会出现不同的效果:

let num = 0; 
for (let i = 1; i < 10; i++) { 
 	if (i % 5 == 0) { 
 		continue; 
 	} 
 	num++; 
} 

console.log(num); // 8 

这一次,console.log 显示 8,即循环被完整执行了 8 次。当 i 等于 5 时,循环会在递增 num 之前退出,但会执行下一次迭代,此时 i 是 6。然后,循环会一直执行到自然结束,即 i 等于 10。最终num 的值是 8 而不是 9,是因为 continue 语句导致它少递增了一次。

break 和 continue 都可以与标签语句一起使用,返回代码中特定的位置。这通常是在嵌套循环中,如下面的例子所示:

let num = 0; 

outermost: 
for (let i = 0; i < 10; i++) { 
 	for (let j = 0; j < 10; j++) { 
 		if (i == 5 && j == 5) { 
 			break outermost; 
 		} 
 		num++; 
 	} 
} 

console.log(num); // 55

在这个例子中,outermost 标签标识的是第一个 for 语句。正常情况下,每个循环执行 10 次,意味着 num++语句会执行 100 次,而循环结束时 console.log 的结果应该是 100。但是,break 语句带来了一个变数,即要退出到的标签。添加标签不仅让 break 退出(使用变量 j 的)内部循环,也会退出(使用变量 i 的)外部循环。当执行到 i 和 j 都等于 5 时,循环停止执行,此时 num 的值是 55。continue语句也可以使用标签,如下面的例子所示:

let num = 0; 

outermost: 
for (let i = 0; i < 10; i++) { 
 	for (let j = 0; j < 10; j++) {
 	 	if (i == 5 && j == 5) { 
 			continue outermost; 
 		} 
 		num++; 
 	} 
} 

console.log(num); // 95 

这一次,continue 语句会强制循环继续执行,但不是继续执行内部循环,而是继续执行外部循环。当 i 和 j 都等于 5 时,会执行 continue,跳到外部循环继续执行,从而导致内部循环少执行 5 次,结果 num 等于 95。

组合使用标签语句和 break、continue 能实现复杂的逻辑,但也容易出错。注意标签要使用描述性强的文本,而嵌套也不要太深。

6.9 The with Statement

The with statement sets the scope of the code within a particular object. The syntax is as follows:

with (expression) statement; 

The with statement was created as a convenience for times when a single object was being coded to over and over again, as in this example:

let qs = location.search.substring(1); 
let hostName = location.hostname; 
let url = location.href; 

Here, the location object is used on every line. This code can be rewritten using the with statement as follows:

with(location) { 
 	let qs = search.substring(1); 
 	let hostName = hostname; 
 	let url = href; 
}

In this rewritten version of the code, the with statement is used in conjunction with the location object. This means that each variable inside the statement is first considered to be a local variable. If it’s not found to be a local variable, the location object is searched to see if it has a property of the
same name. If so, then the variable is evaluated as a property of location.

In strict mode, the with statement is not allowed and is considered a syntax error.

WARNING It is widely considered a poor practice to use the with statement in production code because of its negative performance impact and the difficulty in
debugging code contained in the with statement.

7 函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。

(注意,第10章会更详细地介绍函数)

以下是函数的基本语法:

function functionName(arg0, arg1,...,argN) { 
 statements 
} 

下面是一个例子:

function sayHi(name, message) { 
 console.log("Hello " + name + ", " + message); 
} 

可以通过函数名来调用函数,要传给函数的参数放在括号里(如果有多个参数,则用逗号隔开)。下面是调用函数 sayHi()的示例:

sayHi("Nicholas", "how are you today?");

调用这个函数的输出结果是"Hello Nicholas, how are you today?"。参数 name 和 message在函数内部作为字符串被拼接在了一起,最终通过 console.log 输出到控制台。

ECMAScript 中的函数不需要指定是否返回值。任何函数在任何时间都可以使用 return 语句来返回函数的值,用法是后跟要返回的值。比如:

function sum(num1, num2) { 
 return num1 + num2; 
} 

函数 sum()会将两个值相加并返回结果。注意,除了 return 语句之外没有任何特殊声明表明该函数有返回值。然后就可以这样调用它:

const result = sum(5, 10); 

要注意的是,只要碰到 return 语句,函数就会立即停止执行并退出。因此,return 语句后面的代码不会被执行。比如:

function sum(num1, num2) { 
 return num1 + num2; 
 console.log("Hello world"); // 不会执行
} 

在这个例子中,console.log 不会执行,因为它在 return 语句后面。

一个函数里也可以有多个 return 语句,像这样:

function diff(num1, num2) { 
 	if (num1 < num2) { 
 		return num2 - num1; 
 	} else { 
 		return num1 - num2; 
 	} 
} 

这个 diff()函数用于计算两个数值的差。如果第一个数值小于第二个,则用第二个减第一个;否则,就用第一个减第二个。代码中每个分支都有自己的 return 语句,返回正确的差值。

return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined。这种用法最常用于提前终止函数执行,并不是为了返回值。比如在下面的例子中,console.log 不会执行:

function sayHi(name, message) { 
 	return; 
 	console.log("Hello " + name + ", " + message); // 不会执行
} 

注意 最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值