JavaScript 中
null
和
undefined
是不同的,前者表示一个空值(non-value),必须使用null关键字才能访问,后者是“undefined(未定义)”类型的对象,表示一个未初始化的值,也就是还没有被分配的值。我们之后再具体讨论变量,但有一点可以先简单说明一下,JavaScript 允许声明变量但不对其赋值,一个未被赋值的变量就是
undefined
类型。还有一点需要说明的是,
undefined
实际上是一个不允许修改的常量。
对象
JavaScript 中的对象可以简单理解成“名称-值”对,不难联想 JavaScript 中的对象与下面这些概念类似:
- Python 中的字典
- Perl 和 Ruby 中的散列(哈希)
- C/C++ 中的散列表
- Java 中的 HashMap
- PHP 中的关联数组
这样的数据结构设计合理,能应付各类复杂需求,所以被各类编程语言广泛采用。正因为 JavaScript 中的一切(除了核心类型,core object)都是对象,所以 JavaScript 程序必然与大量的散列表查找操作有着千丝万缕的联系,而散列表擅长的正是高速查找。
“名称”部分是一个 JavaScript 字符串,“值”部分可以是任何 JavaScript 的数据类型——包括对象。这使用户可以根据具体需求,创建出相当复杂的数据结构。
有两种简单方法可以创建一个空对象:
var
obj
=
new
Object
();
和:
var
obj
=
{};
这两种方法在语义上是相同的。第二种更方便的方法叫作“对象字面量(object literal)”法。这种也是 JSON 格式的核心语法,一般我们优先选择第二种方法。
学习 JavaScript 最重要的就是要理解对象和函数两个部分。最简单的函数就像下面这个这么简单:
function
add
(
x
,
y
)
{
var
total
=
x
+
y
;
return
total
;
}
这个例子包括你需要了解的关于基本函数的所有部分。一个 JavaScript 函数可以包含 0 个或多个已命名的变量。函数体中的表达式数量也没有限制。你可以声明函数自己的局部变量。
return
语句在返回一个值并结束函数。如果没有使用
return
语句,或者一个没有值的
return
语句,JavaScript 会返回
undefined
。
关键字
this
。当使用在函数中时,
this
指代当前的对象,也就是调用了函数的对象。如果在一个对象上使用
点或者方括号
来访问属性或方法,这个对象就成了
this
。如果并没有使用“点”运算符调用某个对象,那么
this
将指向全局对象(global object)。
我们引入了另外一个关键字:
new
,它和
this
密切相关。它的作用是创建一个崭新的空对象,然后使用指向那个对象的
this
调用特定的函数。注意,含有
this
的特定函数不会返回任何值,只会修改
this
对象本身。
new
关键字将生成的
this
对象返回给调用方,而被
new
调用的函数成为构造函数。习惯的做法是将这些函数的首字母大写,这样用
new
调用他们的时候就容易识别了。
Person.prototype
是一个可以被
Person
的所有实例共享的对象。它是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问一个
Person
没有定义的属性时,解释器会首先检查这个
Person.prototype
来判断是否存在这样一个属性。所以,任何分配给
Person.prototype
的东西对通过
this
对象构造的实例都是可用的。
原型组成链的一部分。那条链的根节点是
Object.prototype
,它包括
toString()
方法——将对象转换成字符串时调用的方法。
闭包
下面我们将看到的是 JavaScript 中必须提到的功能最强大的抽象概念之一:闭包。但它可能也会带来一些潜在的困惑。那它究竟是做什么的呢?
function
makeAdder
(
a
)
{
return
function
(
b
)
{
return
a
+
b
;
}
}
var
x
=
makeAdder
(
5
);
var
y
=
makeAdder
(
20
);
x
(
6
);
// ?
y
(
7
);
// ?
makeAdder
这个名字本身应该能说明函数是用来做什么的:它创建了一个新的
adder
函数,这个函数自身带有一个参数,它被调用的时候这个参数会被加在外层函数传进来的参数上。
这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以访问外部函数的变量。唯一的不同是,外部函数被返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则
adder
函数将不能工作。也就是说,这里存在
makeAdder
的局部变量的两个不同的“副本”——一个是
a
等于5,另一个是
a
等于20。那些函数的运行结果就如下所示:
x
(
6
);
// 返回 11
y
(
7
);
// 返回 27
下面来说说到底发生了什么。每当 JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它和被传入函数的变量一起被初始化。这与那些保存的所有全局变量和函数的全局对象(global object)类似,但仍有一些很重要的区别,第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(在浏览器里面是当做
window
对象来访问的)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有可以遍历当前的作用域对象里面属性的方法。
所以当调用
makeAdder
时,解释器创建了一个作用域对象,它带有一个属性:
a
,这个属性被当作参数传入
makeAdder
函数。然后
makeAdder
返回一个新创建的函数。通常 JavaScript 的垃圾回收器会在这时回收
makeAdder
创建的作用域对象,但是返回的函数却保留一个指向那个作用域对象的引用。结果是这个作用域对象不会被垃圾回收器回收,直到指向
makeAdder
返回的那个函数对象的引用计数为零。
作用域对象组成了一个名为作用域链(scope chain)的链。它类似于原型(prototype)链一样,被 JavaScript 的对象系统使用。
一个
闭包
就是一个函数和被创建的函数中的作用域对象的组合。
prototype和Object.getPrototypeOf
对于从 Java 或 C++ 转过来的开发人员来说 JavaScript 会有点让人困惑,因为它全部都是动态的,都是运行时,而且不存在类(classes)。所有的都是实例(对象)。即使我们模拟出的 “类(classes)”,也只是一个函数对象。
你可能已经注意到我们的
function A
有一个叫做
prototype
的特殊属性。该特殊属性可与 JavaScript 的 new 操作符一起使用。对原型对象的引用被复制到新实例的内部
[[Prototype]]
属性。例如,当执行
var a1 = new A()
时,JavaScript(在内存中创建对象之前,并且在运行函数
A()
之前)定义了它)设置
a1.[[Prototype]] = A.prototype
。然后当您访问实例的属性时,JavaScript首先会检查它们是否直接存在于该对象上,如果不存在,则会
[[Prototype]]
中查找。这意味着你在
prototype
中定义的所有内容都可以由所有实例有效共享,你甚至可以稍后更改部分
prototype
,并在所有现有实例中显示更改(如果需要)。
像上面的例子中,如果你执行
var a1 = new A(); var a2 = new A(); 那么 a1.doSomething
事实上会指向
Object.getPrototypeOf(a1).doSomething
,它就是你在
A.prototype.doSomething
中定义的内容。比如:
Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething
。
简而言之,
prototype
是用于类型的,而
Object.getPrototypeOf()
是用于实例的(instances),两者功能一致。
[[Prototype]]
看起来就像
递归
引用, 如
a1.doSomething
,
Object.getPrototypeOf(a1).doSomething
,
Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething
等等等, 直到它被找到或
Object.getPrototypeOf
返回 null。
因此,当你执行:
var
o
=
new
Foo
();
JavaScript 实际上执行的是:
var
o
=
new
Object
();
o
.
__proto__
=
Foo
.
prototype
;
Foo
.
call
(
o
);
(或者类似上面这样的),然后当你执行:
o
.
someProp
;
它检查
o
是否具有
someProp
属性。如果没有,它会查找
Object.getPrototypeOf(o).someProp
,如果仍旧没有,它会继续查找
Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp
。