JavaScript 是一种多范式的动态语言,它包含类型、运算符、标准内置( built-in)对象和方法。它的语法来源于 Java 和 C,因此这两种语言的许多语法特性一样适用于 JavaScript。JavaScript 经过原型链而不是类来支持面向对象编程(有关 ES6 类的内容参考这里Classes
,有关对象原型参考见此继承与原型链)。JavaScript一样支持函数编程-由于它们也是对象,函数也能够被保存在变量中,而且像其余对象同样被传递。javascript
先从任何编程语言都不可缺乏的组成部分——“类型”开始。JavaScript 程序能够修改值(value),这些值都有各自的类型。JavaScript 中的类型包括:java
…哦,还有看上去有些…奇怪的 undefined
(未定义)类型和 null
(空)类型。此外还有Array
(数组)类型,以及分别用于表示日期和正则表达式的 Date
(日期)和 RegExp
(正则表达式),这三种类型都是特殊的对象。严格意义上说,Function(函数)也是一种特殊的对象。因此准确来讲,JavaScript 中的类型应该包括这些:node
Number
(数字)git
String
(字符串)程序员
Boolean
(布尔)github
Symbol
(符号)(ES2015 新增)正则表达式
Object
编程
(对象)数组
null
(空)浏览器
undefined
(未定义)
JavaScript 还有一种内置的 Error
(错误)类型。可是,若是咱们继续使用上面的分类,事情便容易得多;因此,如今,咱们先讨论上面这些类型。
根据语言规范,JavaScript 采用“遵循 IEEE 754 标准的双精度 64 位格式”("double-precision 64-bit format IEEE 754 values")表示数字。据此咱们能获得一个有趣的结论,和其余编程语言(如 C 和 Java)不一样,JavaScript 不区分整数值和浮点数值,全部数字在 JavaScript 中均用浮点数值表示,因此在进行数字运算的时候要特别注意。看看下面的例子:
0.1 + 0.2 = 0.30000000000000004
复制代码
在具体实现时,整数值一般被视为32位整型变量,在个别实现(如某些浏览器)中也以32位整型变量的形式进行存储,直到它被用于执行某些32位整型不支持的操做,这是为了便于进行位操做。
JavaScript 支持标准的算术运算符,包括加法、减法、取模(或取余)等等。还有一个以前没有说起的内置对象 Math
(数学对象),用以处理更多的高级数学函数和常数:
Math.sin(3.5);
var circumference = 2 * Math.PI * r;
复制代码
你可使用内置函数 parseInt()
将字符串转换为整型。该函数的第二个可选参数表示字符串所表示数字的基(进制):
parseInt("123", 10); // 123
parseInt("010", 10); // 10
复制代码
一些老版本的浏览器会将首字符为“0”的字符串当作八进制数字,2013 年之前的 JavaScript 实现会返回一个意外的结果:
parseInt("010"); // 8
parseInt("0x10"); // 16
复制代码
这是由于字符串以数字 0 开头,parseInt()
函数会把这样的字符串视做八进制数字;同理,0x开头的字符串则视为十六进制数字。
若是想把一个二进制数字字符串转换成整数值,只要把第二个参数设置为 2 就能够了:
parseInt("11", 2); // 3
复制代码
JavaScript 还有一个相似的内置函数 parseFloat()
,用以解析浮点数字符串,与parseInt()
不一样的地方是,parseFloat()只应用于解析十进制数字。
单元运算符 + 也能够把数字字符串转换成数值:
+ "42"; // 42
+ "010"; // 10
+ "0x10"; // 16
复制代码
若是给定的字符串不存在数值形式,函数会返回一个特殊的值 NaN
(Not a Number 的缩写):
parseInt("hello", 10); // NaN
复制代码
要当心NaN:若是把 NaN
做为参数进行任何数学运算,结果也会是 NaN
:
NaN + 5; //NaN
复制代码
可使用内置函数 isNaN()
来判断一个变量是否为 NaN
:
isNaN(NaN); // true
复制代码
JavaScript 还有两个特殊值:Infinity
(正无穷)和 -Infinity
(负无穷):
1 / 0; // Infinity
-1 / 0; // -Infinity
复制代码
可使用内置函数 isFinite()
来判断一个变量是不是一个有穷数, 若是类型为Infinity
, -Infinity
或 NaN则返回false
:
isFinite(1/0); // false
isFinite(Infinity); // false
isFinite(-Infinity); // false
isFinite(NaN); // false
isFinite(0); // true
isFinite(2e64); // true
isFinite("0"); // true
// 若是是纯数值类型的检测,则返回 false:
Number.isFinite("0"); // false
复制代码
备注:
parseInt()
和parseFloat()
函数会尝试逐个解析字符串中的字符,直到赶上一个没法被解析成数字的字符,而后返回该字符前全部数字字符组成的数字。然而若是使用运算符 "+", 只要字符串中含有没法被解析成数字的字符,该字符串都将被转换成NaN
。可分别使用这两种方法解析“10.2abc”这一字符串,并比较获得的结果,来理解这两种方法的区别。
JavaScript 中的字符串是一串Unicode 字符序列。这对于那些须要和多语种网页打交道的开发者来讲是个好消息。更准确地说,它们是一串UTF-16编码单元的序列,每个编码单元由一个 16 位二进制数表示。每个Unicode字符由一个或两个编码单元来表示。
若是想表示一个单独的字符,只需使用长度为 1 的字符串。
经过访问字符串的 length
(编码单元的个数)属性,能够获得它的长度。
"hello".length; // 5
复制代码
这是咱们第一次碰到 JavaScript 对象。咱们有没有提过你能够像 object 同样使用字符串?是的,字符串也有 methods(方法)能让你操做字符串和获取字符串的信息。
"hello".charAt(0); // "h"
"hello, world".replace("world", "mars"); // "hello, mars"
"hello".toUpperCase(); // "HELLO"
复制代码
与其余类型不一样,JavaScript 中的 null
表示一个空值(non-value),必须使用 null 关键字才能访问,undefined
是一个“undefined(未定义)”类型的对象,表示一个未初始化的值,也就是尚未被分配的值。咱们以后再具体讨论变量,但有一点能够先简单说明一下,JavaScript 容许声明变量但不对其赋值,一个未被赋值的变量就是 undefined
类型。还有一点须要说明的是,undefined
其实是一个不容许修改的常量。
JavaScript 包含布尔类型,这个类型的变量有两个可能的值,分别是 true
和 false
(二者都是关键字)。根据具体须要,JavaScript 按照以下规则将变量转换成布尔类型:
false
、0
、空字符串(""
)、NaN
、null
和 undefined
被转换为 false
true
也可使用 Boolean()
函数进行显式转换:
Boolean(""); // false
Boolean(234); // true
复制代码
不过通常不必这么作,由于 JavaScript 会在须要一个布尔变量时隐式完成这个转换操做(好比在 if
条件语句中)。因此,有时咱们能够把转换成布尔值后的变量分别称为 真值(true values)——即值为 true 和 假值(false values)——即值为 false;也能够分别称为“真的”(truthy)和“假的”(falsy)。
JavaScript 支持包括 &&
(逻辑与)、||
(逻辑或)和!
(逻辑非)在内的一些逻辑运算符。下面会有所提到。
在 JavaScript 中声明一个新变量的方法是使用关键字 let
、const
和 var
:
**let**
语句声明一个块级做用域的本地变量,而且可选的将其初始化为一个值。
let a;
let name = 'Simon';
复制代码
下面是使用 **let**
声明变量做用域的例子:
// myLetVariable is *not* visible out here
for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
// myLetVariable is only visible in here
}
// myLetVariable is *not* visible out here
复制代码
**const**
容许声明一个不可变的常量。这个常量在定义域内老是可见的。
const Pi = 3.14; // 设置 Pi 的值
Pi = 1; // 将会抛出一个错误由于你改变了一个常量的值。
复制代码
**var**
是最多见的声明变量的关键字。它没有其余两个关键字的种种限制。这是由于它是传统上在 JavaScript 声明变量的惟一方法。使用 var 声明的变量在它所声明的整个函数都是可见的。
var a;
var name = "simon";
复制代码
一个使用 var 声明变量的语句块的例子:
// myVarVariable *is* visible out here
for (var myVarVariable = 0; myVarVariable < 5; myVarVariable++) {
// myVarVariable is visible to the whole function
}
// myVarVariable *is* visible out here
复制代码
若是声明了一个变量却没有对其赋值,那么这个变量的类型就是 undefined
。
JavaScript 与其余语言的(如 Java)的重要区别是在 JavaScript 中语句块(blocks)是没有做用域的,只有函数有做用域。所以若是在一个复合语句中(如 if 控制结构中)使用 var 声明一个变量,那么它的做用域是整个函数(复合语句在函数中)。 可是从 ECMAScript Edition 6 开始将有所不一样的, let
和 const
关键字容许你建立块做用域的变量。
JavaScript的算术操做符包括 +
、-
、*
、/
和 %
——求余(与模运算相同)。赋值使用 =
运算符,此外还有一些复合运算符,如 +=
和 -=
,它们等价于 x = x *operator* y
。
x += 5; // 等价于 x = x + 5;
复制代码
可使用 ++
和 --
分别实现变量的自增和自减。二者均可以做为前缀或后缀操做符使用。
+
操做符还能够用来链接字符串:
"hello" + " world"; // hello world
复制代码
若是你用一个字符串加上一个数字(或其余值),那么操做数都会被首先转换为字符串。以下所示:
"3" + 4 + 5; // 345
3 + 4 + "5"; // 75
复制代码
这里不难看出一个实用的技巧——经过与空字符串相加,能够将某个变量快速转换成字符串类型。
JavaScript 中的比较操做使用 <
、>
、<=
和 >=
,这些运算符对于数字和字符串都通用。相等的比较稍微复杂一些。由两个“=
(等号)”组成的相等运算符有类型自适应的功能,具体例子以下:
123 == "123" // true
1 == true; // true
复制代码
若是在比较前不须要自动类型转换,应该使用由三个“=
(等号)”组成的相等运算符:
1 === true; //false
123 === "123"; // false
复制代码
JavaScript 还支持 !=
和 !==
两种不等运算符,具体区别与两种相等运算符的区别相似。
JavaScript 还提供了 位操做符。
JavaScript 的控制结构与其余类 C 语言相似。可使用 if
和 else
来定义条件语句,还能够连起来使用:
var name = "kittens";
if (name == "puppies") {
name += "!";
} else if (name == "kittens") {
name += "!!";
} else {
name = "!" + name;
}
name == "kittens!!"; // true
复制代码
JavaScript 支持 while
循环和 do-while
循环。前者适合常见的基本循环操做,若是须要循环体至少被执行一次则可使用 do-while
:
while (true) {
// 一个无限循环!
}
var input;
do {
input = get_input();
} while (inputIsNotValid(input))
复制代码
JavaScript 的 for
循环与 C 和 Java 中的相同:使用时能够在一行代码中提供控制信息。
for (var i = 0; i < 5; i++) {
// 将会执行五次
}
复制代码
JavaScript 也还包括其余两种重要的 for 循环: for
...of
for (let value of array) {
// do something with value
}
复制代码
和 for
...in
:
for (let property in object) {
// do something with object property
}
复制代码
&&
和 ||
运算符使用短路逻辑(short-circuit logic),是否会执行第二个语句(操做数)取决于第一个操做数的结果。在须要访问某个对象的属性时,使用这个特性能够事先检测该对象是否为空:
var name = o && o.getName();
复制代码
或用于缓存值(当错误值无效时):
var name = cachedName || (cachedName = getName());
复制代码
相似地,JavaScript 也有一个用于条件表达式的三元操做符:
var allowed = (age > 18) ? "yes" : "no";
复制代码
在须要多重分支时可使用 基于一个数字或字符串的switch
语句:
switch(action) {
case 'draw':
drawIt();
break;
case 'eat':
eatIt();
break;
default:
doNothing();
}
复制代码
若是你不使用 break
语句,JavaScript 解释器将会执行以后 case
中的代码。除非是为了调试,通常你并不须要这个特性,因此大多数时候不要忘了加上 break。
switch(a) {
case 1: // 继续向下
case 2:
eatIt();
break;
default:
doNothing();
}
复制代码
default
语句是可选的。switch
和 case
均可以使用须要运算才能获得结果的表达式;在 switch
的表达式和 case
的表达式是使用 ===
严格相等运算符进行比较的:
switch(1 + 3){
case 2 + 2:
yay();
break;
default:
neverhappens();
}
复制代码
JavaScript 中的对象,Object,能够简单理解成“名称-值”对(而不是键值对:如今,ES 2015 的映射表(Map),比对象更接近键值对),不难联想 JavaScript 中的对象与下面这些概念相似:
这样的数据结构设计合理,能应付各种复杂需求,因此被各种编程语言普遍采用。正由于 JavaScript 中的一切(除了核心类型,core object)都是对象,因此 JavaScript 程序必然与大量的散列表查找操做有着千丝万缕的联系,而散列表擅长的正是高速查找。
“名称”部分是一个 JavaScript 字符串,“值”部分能够是任何 JavaScript 的数据类型——包括对象。这使用户能够根据具体需求,建立出至关复杂的数据结构。
有两种简单方法能够建立一个空对象:
var obj = new Object();
复制代码
和:
var obj = {};
复制代码
这两种方法在语义上是相同的。第二种更方便的方法叫做“对象字面量(object literal)”法。这种也是 JSON 格式的核心语法,通常咱们优先选择第二种方法。
“对象字面量”也能够用来在对象实例中定义一个对象:
var obj = {
name: "Carrot",
"for": "Max",//'for' 是保留字之一,使用'_for'代替
details: {
color: "orange",
size: 12
}
}
复制代码
对象的属性能够经过链式(chain)表示方法进行访问:
obj.details.color; // orange
obj["details"]["size"]; // 12
复制代码
下面的例子建立了一个对象原型,**Person**
,和这个原型的实例,You。
function Person(name, age) {
this.name = name;
this.age = age;
}
// 定义一个对象
var You = new Person("You", 24);
// 咱们建立了一个新的 Person,名称是 "You"
// ("You" 是第一个参数, 24 是第二个参数..)
复制代码
完成建立后,对象属性能够经过以下两种方式进行赋值和访问:
obj.name = "Simon"
var name = obj.name;
复制代码
和:
// bracket notation
obj['name'] = 'Simon';
var name = obj['name'];
// can use a variable to define a key
var user = prompt('what is your key?')
obj[user] = prompt('what is its value?')
复制代码
这两种方法在语义上也是相同的。第二种方法的优势在于属性的名称被看做一个字符串,这就意味着它能够在运行时被计算,缺点在于这样的代码有可能没法在后期被解释器优化。它也能够被用来访问某些以预留关键字做为名称的属性的值:
obj.for = "Simon"; // 语法错误,由于 for 是一个预留关键字
obj["for"] = "Simon"; // 工做正常
复制代码
**注意:**从 EcmaScript 5 开始,预留关键字能够做为对象的属性名(reserved words may be used as object property names "in the buff")。 这意味着当定义对象字面量时不须要用双引号了。参见 ES5 Spec.
关于对象和原型的详情参见: Object.prototype. 解释对象原型和对象原型链能够参见:继承与原型链。
**注意:**从 EcmaScript 6 开始,对象键能够在建立时使用括号表示法由变量定义。{[phoneType]: 12345} 能够用来替换 var userPhone = {}; userPhone[phoneType] = 12345
.
JavaScript 中的数组是一种特殊的对象。它的工做原理与普通对象相似(以数字为属性名,但只能经过[]
来访问),但数组还有一个特殊的属性——length
(长度)属性。这个属性的值一般比数组最大索引大 1。
建立数组的传统方法是:
var a = new Array();
a[0] = "dog";
a[1] = "cat";
a[2] = "hen";
a.length; // 3
复制代码
使用数组字面量(array literal)法更加方便:
var a = ["dog", "cat", "hen"];
a.length; // 3
复制代码
注意,Array.length
并不老是等于数组中元素的个数,以下所示:
var a = ["dog", "cat", "hen"];
a[100] = "fox";
a.length; // 101
复制代码
记住:数组的长度是比数组最大索引值多一的数。
若是试图访问一个不存在的数组索引,会获得 undefined
:
typeof(a[90]); // undefined
复制代码
能够经过以下方式遍历一个数组:
for (var i = 0; i < a.length; i++) {
// Do something with a[i]
}
复制代码
ES2015 引入了更加简洁的 for
...of
循环,能够用它来遍历可迭代对象,例如数组:
for (const currentValue of a) {
// Do something with currentValue
}
复制代码
遍历数组的另外一种方法是使用 for...in
循环, 然而这并非遍历数组元素而是数组的索引。注意,若是哪一个家伙直接向 Array.prototype
添加了新的属性,使用这样的循环这些属性也一样会被遍历。因此并不推荐使用这种方法遍历数组:
for (var i in a) {
// Do something with a[i]
}
复制代码
ECMAScript 5 增长了另外一个遍历数组的方法,forEach()
:
["dog", "cat", "hen"].forEach(function(currentValue, index, array) {
// Do something with currentValue or array[index]
});
复制代码
若是想在数组后追加元素,只须要:
a.push(item);
复制代码
除了 forEach()
和 push()
,Array(数组)类还自带了许多方法。建议查看 Array 方法的完整文档。
方法名称 | 描述 |
---|---|
a.toString() |
返回一个包含数组中全部元素的字符串,每一个元素经过逗号分隔。 |
a.toLocaleString() |
根据宿主环境的区域设置,返回一个包含数组中全部元素的字符串,每一个元素经过逗号分隔。 |
a.concat(item1[, item2[, ...[, itemN]]]) |
返回一个数组,这个数组包含原先 a 和 item一、item二、……、itemN 中的全部元素。 |
a.join(sep) |
返回一个包含数组中全部元素的字符串,每一个元素经过指定的 sep 分隔。 |
a.pop() |
删除并返回数组中的最后一个元素。 |
a.push(item1, ..., itemN) |
将 item一、item二、……、itemN 追加至数组 a 。 |
a.reverse() |
数组逆序(会更改原数组 a )。 |
a.shift() |
删除并返回数组中第一个元素。 |
a.slice(start, end) |
返回子数组,以 a[start] 开头,以 a[end] 前一个元素结尾。 |
a.sort([cmpfn]) |
依据可选的比较函数 cmpfn 进行排序,若是未指定比较函数,则按字符顺序比较(即便被比较元素是数字)。 |
a.splice(start, delcount[, item1[, ...[, itemN]]]) |
从 start 开始,删除 delcount 个元素,而后插入全部的 item 。 |
a.unshift(item1[, item2[, ...[, itemN]]]) |
将 item 插入数组头部,返回数组新长度(考虑 undefined )。 |
学习 JavaScript 最重要的就是要理解对象和函数两个部分。最简单的函数就像下面这个这么简单:
function add(x, y) {
var total = x + y;
return total;
}
复制代码
这个例子包括你须要了解的关于基本函数的全部部分。一个 JavaScript 函数能够包含 0 个或多个已命名的变量。函数体中的表达式数量也没有限制。你能够声明函数本身的局部变量。return
语句在返回一个值并结束函数。若是没有使用 return
语句,或者一个没有值的 return
语句,JavaScript 会返回 undefined
。
已命名的参数更像是一个指示而没有其余做用。若是调用函数时没有提供足够的参数,缺乏的参数会被 undefined
替代。
add(); // NaN
// 不能在 undefined 对象上进行加法操做
复制代码
你还能够传入多于函数自己须要参数个数的参数:
add(2, 3, 4); // 5
// 将前两个值相加,4 被忽略了
复制代码
这看上去有点蠢。函数其实是访问了函数体中一个名为 arguments
的内部对象,这个对象就如同一个相似于数组的对象同样,包括了全部被传入的参数。让咱们重写一下上面的函数,使它能够接收任意个数的参数:
function add() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum;
}
add(2, 3, 4, 5); // 14
复制代码
这跟直接写成 2 + 3 + 4 + 5
也没什么区别。咱们仍是建立一个求平均数的函数吧:
function avg() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum / arguments.length;
}
avg(2, 3, 4, 5); // 3.5
复制代码
这个就有用多了,可是却有些冗长。为了使代码变短一些,咱们可使用剩余参数来替换arguments的使用。在这方法中,咱们能够传递任意数量的参数到函数中同时尽可能减小咱们的代码。这个剩余参数操做符在函数中以:...variable 的形式被使用,它将包含在调用函数时使用的未捕获整个参数列表到这个变量中。咱们一样也能够将 for 循环替换为 for...of 循环来返回咱们变量的值。
function avg(...args) {
var sum = 0;
for (let value of args) {
sum += value;
}
return sum / args.length;
}
avg(2, 3, 4, 5); // 3.5
复制代码
在上面这段代码中,全部被传入该函数的参数都被变量 args 所持有。
须要注意的是,不管“剩余参数操做符”被放置到函数声明的哪里,它都会把除了本身以前的全部参数存储起来。好比函数:function avg(firstValue, ...args) 会把传入函数的第一个值存入 firstValue,其余的参数存入 args。这是虽然一个颇有用的语言特性,却也会带来新的问题。avg()
函数只接受逗号分开的参数列表 -- 可是若是你想要获取一个数组的平均值怎么办?一种方法是将函数按照以下方式重写:
function avgArray(arr) {
var sum = 0;
for (var i = 0, j = arr.length; i < j; i++) {
sum += arr[i];
}
return sum / arr.length;
}
avgArray([2, 3, 4, 5]); // 3.5
复制代码
但若是能重用咱们已经建立的那个函数不是更好吗?幸运的是 JavaScript 容许你经过任意函数对象的 apply()
方法来传递给它一个数组做为参数列表。
avg.apply(null, [2, 3, 4, 5]); // 3.5
复制代码
传给 apply()
的第二个参数是一个数组,它将被看成 avg()
的参数列表使用,至于第一个参数 null
,咱们将在后面讨论。这也正说明了一个事实——函数也是对象。
经过使用展开语法,你也能够得到一样的效果。
好比说:avg(...numbers)
JavaScript 容许你建立匿名函数:
var avg = function() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum / arguments.length;
};
复制代码
这个函数在语义上与 function avg()
相同。你能够在代码中的任何地方定义这个函数,就像写普通的表达式同样。基于这个特性,有人发明出一些有趣的技巧。与 C 中的块级做用域相似,下面这个例子隐藏了局部变量:
var a = 1;
var b = 2;
(function() {
var b = 3;
a += b;
})();
a; // 4
b; // 2
复制代码
JavaScript 容许以递归方式调用函数。递归在处理树形结构(好比浏览器 DOM)时很是有用。
function countChars(elm) {
if (elm.nodeType == 3) { // TEXT_NODE 文本节点
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += countChars(child);
}
return count;
}
复制代码
这里须要说明一个潜在问题——既然匿名函数没有名字,那该怎么递归调用它呢?在这一点上,JavaScript 容许你命名这个函数表达式。你能够命名当即调用的函数表达式(IIFE——Immediately Invoked Function Expression),以下所示:
var charsInBody = (function counter(elm) {
if (elm.nodeType == 3) { // 文本节点
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += counter(child);
}
return count;
})(document.body);
复制代码
如上所提供的函数表达式的名称的做用域仅仅是该函数自身。这容许引擎去作更多的优化,而且这种实现更可读、友好。该名称也显示在调试器和一些堆栈跟踪中,节省了调试时的时间。
须要注意的是 JavaScript 函数是它们自己的对象——就和 JavaScript 其余一切同样——你能够给它们添加属性或者更改它们的属性,这与前面的对象部分同样。
**备注:**关于 JavaScript 中面向对象编程更详细的信息,请参考 JavaScript 面向对象简介。
在经典的面向对象语言中,对象是指数据和在这些数据上进行的操做的集合。与 C++ 和 Java 不一样,JavaScript 是一种基于原型的编程语言,并无 class 语句,而是把函数用做类。那么让咱们来定义一我的名对象,这个对象包括人的姓和名两个域(field)。名字的表示有两种方法:“名 姓(First Last)”或“姓, 名(Last, First)”。使用咱们前面讨论过的函数和对象概念,能够像这样完成定义:
function makePerson(first, last) {
return {
first: first,
last: last
}
}
function personFullName(person) {
return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {
return person.last + ', ' + person.first
}
s = makePerson("Simon", "Willison");
personFullName(s); // Simon Willison
personFullNameReversed(s); // Willison, Simon
复制代码
上面的写法虽然能够知足要求,可是看起来很麻烦,由于须要在全局命名空间中写不少函数。既然函数自己就是对象,若是须要使一个函数隶属于一个对象,那么不可贵到:
function makePerson(first, last) {
return {
first: first,
last: last,
fullName: function() {
return this.first + ' ' + this.last;
},
fullNameReversed: function() {
return this.last + ', ' + this.first;
}
}
}
s = makePerson("Simon", "Willison");
s.fullName(); // Simon Willison
s.fullNameReversed(); // Willison, Simon
复制代码
上面的代码里有一些咱们以前没有见过的东西:关键字 this
。当使用在函数中时,this
指代当前的对象,也就是调用了函数的对象。若是在一个对象上使用点或者方括号来访问属性或方法,这个对象就成了 this
。若是并无使用“点”运算符调用某个对象,那么 this
将指向全局对象(global object)。这是一个常常出错的地方。例如:
s = makePerson("Simon", "Willison");
var fullName = s.fullName;
fullName(); // undefined undefined
复制代码
当咱们调用 fullName()
时,this
其实是指向全局对象的,并无名为 first
或 last
的全局变量,因此它们两个的返回值都会是 undefined
。
下面使用关键字 this
改进已有的 makePerson
函数:
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = function() {
return this.first + ' ' + this.last;
}
this.fullNameReversed = function() {
return this.last + ', ' + this.first;
}
}
var s = new Person("Simon", "Willison");
复制代码
咱们引入了另一个关键字:new
,它和 this
密切相关。它的做用是建立一个崭新的空对象,而后使用指向那个对象的 this
调用特定的函数。注意,含有 this
的特定函数不会返回任何值,只会修改 this
对象自己。new
关键字将生成的 this
对象返回给调用方,而被 new
调用的函数称为构造函数。习惯的作法是将这些函数的首字母大写,这样用 new
调用他们的时候就容易识别了。
不过,这个改进的函数仍是和上一个例子同样,在单独调用fullName()
时,会产生相同的问题。
咱们的 Person 对象如今已经至关完善了,但还有一些不太好的地方。每次咱们建立一个 Person 对象的时候,咱们都在其中建立了两个新的函数对象——若是这个代码能够共享不是更好吗?
function personFullName() {
return this.first + ' ' + this.last;
}
function personFullNameReversed() {
return this.last + ', ' + this.first;
}
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = personFullName;
this.fullNameReversed = personFullNameReversed;
}
复制代码
这种写法的好处是,咱们只须要建立一次方法函数,在构造函数中引用它们。那是否还有更好的方法呢?答案是确定的。
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.fullName = function() {
return this.first + ' ' + this.last;
}
Person.prototype.fullNameReversed = function() {
return this.last + ', ' + this.first;
}
复制代码
Person.prototype
是一个能够被Person
的全部实例共享的对象。它是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问一个 Person
没有定义的属性时,解释器会首先检查这个 Person.prototype
来判断是否存在这样一个属性。因此,任何分配给 Person.prototype
的东西对经过 this
对象构造的实例都是可用的。
这个特性功能十分强大,JavaScript 容许你在程序中的任什么时候候修改原型(prototype)中的一些东西,也就是说你能够在运行时(runtime)给已存在的对象添加额外的方法:
s = new Person("Simon", "Willison");
s.firstNameCaps(); // TypeError on line 1: s.firstNameCaps is not a function
Person.prototype.firstNameCaps = function() {
return this.first.toUpperCase()
}
s.firstNameCaps(); // SIMON
复制代码
有趣的是,你还能够给 JavaScript 的内置函数原型(prototype)添加东西。让咱们给 String
添加一个方法用来返回逆序的字符串:
var s = "Simon";
s.reversed(); // TypeError on line 1: s.reversed is not a function
String.prototype.reversed = function() {
var r = "";
for (var i = this.length - 1; i >= 0; i--) {
r += this[i];
}
return r;
}
s.reversed(); // nomiS
复制代码
定义新方法也能够在字符串字面量上用(string literal)。
"This can now be reversed".reversed(); // desrever eb won nac sihT
复制代码
正如我前面提到的,原型组成链的一部分。那条链的根节点是 Object.prototype
,它包括 toString()
方法——将对象转换成字符串时调用的方法。这对于调试咱们的 Person
对象颇有用:
var s = new Person("Simon", "Willison");
s; // [object Object]
Person.prototype.toString = function() {
return '<Person: ' + this.fullName() + '>';
}
s.toString(); // <Person: Simon Willison>
复制代码
你是否还记得以前咱们说的 avg.apply()
中的第一个参数 null
?如今咱们能够回头看看这个东西了。apply()
的第一个参数应该是一个被看成 this
来看待的对象。下面是一个 new
方法的简单实现:
function trivialNew(constructor, ...args) {
var o = {}; // 建立一个对象
constructor.apply(o, args);
return o;
}
复制代码
这并非 new
的完整实现,由于它没有建立原型(prototype)链。想举例说明 new 的实现有些困难,由于你不会常常用到这个,可是适当了解一下仍是颇有用的。在这一小段代码里,...args
(包括省略号)叫做剩余参数(rest arguments)。如名所示,这个东西包含了剩下的参数。
所以,调用
var bill = trivialNew(Person, "William", "Orange");
复制代码
可认为和调用以下语句是等效的
var bill = new Person("William", "Orange");
复制代码
apply()
有一个姐妹函数,名叫 call
,它也能够容许你设置 this
,但它带有一个扩展的参数列表而不是一个数组。
function lastNameCaps() {
return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
// 和如下方式等价
s.lastNameCaps = lastNameCaps;
s.lastNameCaps();
复制代码
JavaScript 容许在一个函数内部定义函数,这一点咱们在以前的 makePerson()
例子中也见过。关于 JavaScript 中的嵌套函数,一个很重要的细节是,它们能够访问父函数做用域中的变量:
function parentFunc() {
var a = 1;
function nestedFunc() {
var b = 4; // parentFunc 没法访问 b
return a + b;
}
return nestedFunc(); // 5
}
复制代码
若是某个函数依赖于其余的一两个函数,而这一两个函数对你其他的代码没有用处,你能够将它们嵌套在会被调用的那个函数内部,这样作能够减小全局做用域下的函数的数量,这有利于编写易于维护的代码。
这也是一个减小使用全局变量的好方法。当编写复杂代码时,程序员每每试图使用全局变量,将值共享给多个函数,但这样作会使代码很难维护。内部函数能够共享父函数的变量,因此你可使用这个特性把一些函数捆绑在一块儿,这样能够有效地防止“污染”你的全局命名空间——你能够称它为“局部全局(local global)”。虽然这种方法应该谨慎使用,但它确实颇有用,应该掌握。
闭包是 JavaScript 中最强大的抽象概念之一——但它也是最容易形成困惑的。它到底是作什么的呢?
function makeAdder(a) {
return function(b) {
return a + b;
}
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?
复制代码
makeAdder
这个名字自己,便应该能说明函数是用来作什么的:它会用一个参数来建立一个新的“adder”函数,再用另外一个参数来调用被建立的函数时,makeAdder
会将一前一后两个参数相加。
从被建立的函数的视角来看的话,这两个参数的来源问题会更显而易见:新函数自带一个参数——在新函数被建立时,便钦定、钦点了前一个参数(如上方代码中的 a、5 和 20,参考 makeAdder
的结构,它应当位于新函数外部);新函数被调用时,又接收了后一个参数(如上方代码中的 b、6 和 7,位于新函数内部)。最终,新函数被调用的时候,前一个参数便会和由外层函数传入的后一个参数相加。
这里发生的事情和前面介绍过的内嵌函数十分类似:一个函数被定义在了另一个函数的内部,内部函数能够访问外部函数的变量。惟一的不一样是,外部函数已经返回了,那么常识告诉咱们局部变量“应该”再也不存在。可是它们却仍然存在——不然 adder
函数将不能工做。也就是说,这里存在 makeAdder
的局部变量的两个不一样的“副本”——一个是 a
等于 5,另外一个是 a
等于 20。那些函数的运行结果就以下所示:
x(6); // 返回 11
y(7); // 返回 27
复制代码
下面来讲说,到底发生了什么了不起的事情。每当 JavaScript 执行一个函数时,都会建立一个做用域对象(scope object),用来保存在这个函数中建立的局部变量。它使用一切被传入函数的变量进行初始化(初始化后,它包含一切被传入函数的变量)。这与那些保存的全部全局变量和函数的全局对象(global object)相相似,但仍有一些很重要的区别:第一,每次函数被执行的时候,就会建立一个新的,特定的做用域对象;第二,与全局对象(如浏览器的 window
对象)不一样的是,你不能从 JavaScript 代码中直接访问做用域对象,也没有 能够遍历当前做用域对象中的属性 的方法。
因此,当调用 makeAdder
时,解释器建立了一个做用域对象,它带有一个属性:a
,这个属性被看成参数传入 makeAdder
函数。而后 makeAdder
返回一个新建立的函数(暂记为 adder
)。一般,JavaScript 的垃圾回收器会在这时回收 makeAdder
建立的做用域对象(暂记为 b
),可是,makeAdder
的返回值,新函数 adder
,拥有一个指向做用域对象 b
的引用。最终,做用域对象 b
不会被垃圾回收器回收,直到没有任何引用指向新函数 adder
。
做用域对象组成了一个名为做用域链(scope chain)的(调用)链。它和 JavaScript 的对象系统使用的原型(prototype)链相相似。
一个闭包,就是 一个函数 与其 被建立时所带有的做用域对象 的组合。闭包容许你保存状态——因此,它们能够用来代替对象。这个 StackOverflow 帖子里有一些关于闭包的详细介绍。