这一篇继续分享第三章的内容,第三章的内容快要结束了。
3.7 函数
函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行(也就是说,函数的好处是可以封装重复的代码)。ECMAScript中的函数使用function 关键字声明,后跟一组参数,然后是函数体。
以下是函数的基本语法:
function functionName(arg0, arg1,...,argN) {statements
}
可以通过函数名来调用函数,要传给函数的参数放在括号里(如果有多个参数,则用逗号隔开)。下面是调用函数 sayHi() 的示例:
sayHi("Nicholas", "how are you today?");
ECMAScript中的函数不需要指定是否返回值。任何函数在任何时间都可以使用 return 语句来返回函数的值,用法是后跟要返回的值。比如:
function sum(num1, num2) {return num1 + num2;
}
函数 sum() 会将两个值相加并返回结果。注意,除了 return语句之外没有任何特殊声明表明该函数有返回值(也就是要指定函数的返回值的时候,只能使用return语句)。然后就可以这样用它:(也就是用一个变量来接收函数的返回值)
const result = sum(5, 10);
要注意的是,只要碰到 return 语句,函数就会立即停止执行并退出。因此, return 语句后面的代码不会被执行。
一个函数里也可以有多个 return 语句,像这样:
function diff(num1, num2) {if (num1 < num2) {return num2 - num1;} else {return num1 - num2;}
}
这个 diff() 函数用于计算两个数值的差。如果第一个数值小于第二个,则用第二个减第一个;否则,就用第一个减第二个。代码中每个分支都有自己的 return 语句,返回正确的差值。(也就是要根据不同状况返回不同值的时候,可以把return写到if语句里)
return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined 。这种用法最常用于提前终止函数执行,并不是为了返回值。
注意 最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。
严格模式对函数也有一些限制:
函数不能以 eval 或 arguments 作为名称;(因为eval是一个特殊的函数,这个名字已经被使用了;arguments这个名字也已经被使用了)
函数的参数不能叫 eval 或 arguments ;
两个函数的参数不能叫同一个名称。(也就是说,同一个函数的两个参数不能同名)
如果违反上述规则,则会导致语法错误,代码也不会执行。
3.8 小结
JavaScript的核心语言特性在ECMA-262中以伪语言ECMAScript的形式来定义。ECMAScript包含所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的机制。理解ECMAScript及其复杂的细节是完全理解浏览器中JavaScript的关键。下面总结一下ECMAScript中的基本元素。
ECMAScript中的基本数据类型包括 Undefined 、 Null 、Boolean 、 Number 、 String 和 Symbol 。
与其他语言不同,ECMAScript不区分整数和浮点值,只有Number 一种数值数据类型。
Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
严格模式为这门语言中某些容易出错的部分施加了限制。
ECMAScript提供了C语言和类C语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如if 语句、 for 语句和 switch 语句等。ECMAScript中的函数与其他语言中的函数不一样
不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。不指定返回值的函数实际上会返回特殊值 undefined 。
第 4 章 变量、作用域与内存
相比于其他语言,JavaScript中的变量可谓独树一帜。正如ECMA-262所规定的,JavaScript变量是松散类型的,而且变量不过就是特定时间点一个特定值的名称而已。由于没有规则定义变量必须包含什么数据类型,变量的值和数据类型在脚本生命期内可以改变。这样的变量很有意思,很强大,当然也有不少问题。
4.1 原始值与引用值
ECMAScript变量可以包含两种不同类型的数据:原始值和引用值。原始值(primitive value)就是最简单的数据,引用值(referencevalue)则是由多个值构成的对象。
在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。上一章讨论了6种原始值: Undefined 、 Null 、Boolean 、 Number 、 String 和 Symbol 。保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象。与其他语言不同,JavaScript不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。
在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。
注意 在很多语言中,字符串是使用对象表示的,因此被认为是引用类型。ECMAScript打破了这个惯例。
4.1.1 动态属性
原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大有不同。对于引用值而言,可以随时添加、修改和删除其属性和方法。
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"
这里,首先创建了一个对象,并把它保存在变量 person 中。然后,给这个对象添加了一个名为 name 的属性,并给这个属性赋值了一个字符串 "Nicholas" 。在此之后,就可以访问这个新属性,直到对象被销毁或属性被显式地删除。
原始值不能有属性,尽管尝试给原始值添加属性不会报错。
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
在此,代码想给字符串 name 定义一个 age 属性并给该属性赋值27。紧接着在下一行,属性不见了。记住,只有引用值可以动态添加后面可以使用的属性。
注意,原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则JavaScript会创建一个 Object 类型的实例,其行为类似原始值。(本质上仍然是个对象)比如下面的这两个值:
let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object
4.1.2 复制值
除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。
let num1 = 5;
let num2 = num1;
这里, num1 包含数值5。当把 num2 初始化为 num1 时,num2 也会得到数值5。这个值跟存储在 num1 中的5是完全独立的,因为它是那个值的副本。(通过变量num1,把5这个值复制到了num2这里,由于是值被复制了,所以新的值不影响原来变量的值)这两个变量可以独立使用,互不干扰。
在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"
在这个例子中,变量 obj1 保存了一个新对象的实例。然后,这个值被复制到 obj2 ,此时两个变量都指向了同一个对象。在给obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为它们都指向同一个对象。
4.1.3 传递参数
ECMAScript中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。对很多开发者来说,这一块可能会不好理解,毕竟变量有按值和按引用访问,而传参则只有按值传递。
在按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者用ECMAScript的话说,就是 arguments 对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反映到函数外部。
function addTen(num) {num += 10;return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20,没有变化
console.log(result); // 30
这里,函数 addTen() 有一个参数 num ,它其实是一个局部变量。在调用时,变量 count 作为参数传入。 count 的值是20,这个值被复制到参数 num 以便在 addTen() 内部使用。在函数内部,参数num的值被加上了10,但这不会影响函数外部的原始变量count 。参数 num 和变量 count 互不干扰,它们只不过碰巧保存了一样的值。如果 num 是按引用传递的,那么 count 的值也会被修改为30。这个事实在使用数值这样的原始值时是非常明显的。但是,如果变量中传递的是对象,就没那么清楚了。比如,再看这个例子:
function setName(obj) {obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
这一次,我们创建了一个对象并把它保存在变量 person 中。然后,这个对象被传给 setName() 方法,并被复制到参数 obj 中。在函数内部, obj 和 person 都指向同一个对象。结果就是,即使对象是按值传进函数的, obj 也会通过引用访问对象。(这里应该这么理解:保存一个对象时,变量里存储的其实是对象的地址(即引用)。传入函数的是变量的值,也就是对象的地址,所以函数内部的obj跟person指向了同一个对象)当函数内部给 obj 设置了 name 属性时,函数外部的对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上。很多开发者错误地认为,当在局部作用域中修改对象而变化反映到全局时,就意味着参数是按引用传递的。
为证明对象是按值传递的,我们再来看看下面这个修改后的例子:
function setName(obj) {obj.name = "Nicholas";obj = new Object();obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
这个例子前后唯一的变化就是 setName() 中多了两行代码,将 obj 重新定义为一个有着不同 name 的新对象。当 person 传入 setName() 时,其 name 属性被设置为 "Nicholas" 。然后变量 obj 被设置为一个新对象且 name 属性被设置为 "Greg" 。如果 person 是按引用传递的,那么 person 应该自动将指针改为指向 name 为 "Greg" 的对象。可是,当我们再次访问
person.name 时,它的值是 "Nicholas" ,这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
注意 ECMAScript中函数的参数就是局部变量