第二章 语法
数字
JavaScript 只有一个数字类型,它在内部被表示为64位的浮点数,和Java的double数字类型一样。它没有分离出整数类型,所以1和1.0的值相同,完全避免了一大堆因数字类型导致的错误。
字符串
字符串字面量可以被包在一对单引号或双引号中,可能包含0个或多个字符。
JavaScript没有字符类型,要表示一个字符,只需要创建一个字符的字符串即可。
字符串是不可变的,一旦字符串被创建,就永远无法改变它,两个包含着完全相同的字符且字符顺序也相同的字符串被认为是相同的字符串。
'c' + 'a' + 't' === 'cat' // true
语句
每个<script>标签提供一个被编译且立即执行的编译单元
与其他语言不同,JavaScript中的代码块不会创建新的作用域,因此变量应该被定义在函数的头部,而不是在代码块中。
var scope = 'global'
function f() {
console.log(scope) // 输出'undefined',而不是'global'
var scope = 'local' // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope) // 输出'local'
}
等价于:
var scope = 'global'
function f() {
var scope
console.log(scope) // 输出'undefined',而不是'global'
scope = 'local' // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope) // 输出'local'
}
布尔值的判断中,以下列出的值被当作假,除此外,均为真:
- false
- null
- 空字符串' '
- 数字 0
- 数字 NaN
typeof 运算符产生的值有:
- number
- string
- boolean
- undefined
- function
- object
第三章 对象
JavaScript的简单数据类型包括数字、字符串、布尔值(true和false)、null值和undefined值,其他所有的值都是对象。
数字、字符串、布尔值“貌似”对象,因为它们拥有方法,但它们是不可变的。
对象是属性的容器,其中每个属性都拥有名字和值。属性的名字和属性的值没有限制。对象适合用于汇集和管理数据。对象可以包含其他对象,所以它们可以容易地表示成树状或图形结构。
对象
如果属性名是一个合法的JavaScript标识符且不是保留字,则并不强制要求用引号括住属性名。
var empty_object = {}
var stooge = {
"first-name": "Jerome", // 连接符(-)是不合法的,要加引号
last_name: "Howard", // 下划线(_)是合法的,可以不用加引号
}
对象嵌套
var flight = {
airline: "Oceanic",
number: 815,
departure: {
IATA: "SYD",
time: "2004-09-22 14:55",
city: "Sydney"
},
arrival: {
IATA: "LAX"
time: "2004-09-23 10:42",
city: "Los Angeles"
}
}
检索
stooge["first-name"] // 采用在[]后缀中括住一个字符串表达式
flight.departure.IATA // 如果字符串表达式是一个字符串字面量,并且是一个合法的JavaScript标识符且不是保留字,那么可以用 . 表示法代替
// 尝试检索一个并不存在的成员属性的值,将返回undefined
stooge["middle-name"] // undefined,
flight.status // undefined,同上
// ||运算符可以用来填充默认值
var middle = stooge["middle-name"] || "(none)"
// 尝试从undefined的成员属性中取值将会导致TypeError异常。这时可以通过&&运算符来避免错误。
flight.equipment // undefined
flight.equipment.model // throw "TypeError"
flight.equipment && flight.equipment.model // undefined
更新
对象里的值可以通过赋值语句来更新。如果属性名已经存在于对象里,那么这个属性的值就会被替换。如果对象之前没有拥有那个属性名,那么该属性就被扩充到对象中。
引用
对象通过引用来传递,它们永远不会被复制。
原型
每个对象都连接到一个原型对象,并且它可以从中继承属性。所有通过对象字面量创建的对象连接到Object.prototype,它是JavaScript中的标配对象。
当创建一个新对象时,我们可以选择某个对象作为它的原型。
if (typeof Object.beget !=== 'function') {
Object.create = function(o) {
var F = function() {}
F.prototype = o
return new F()
}
}
var another_stooge = Object.create(stooge)
// 原型连接在更新时是不起作用的。当我们对某个对象作出改变时,不会触及该对象的原型
another_stooge['first-name'] = 'Harry'
another_stooge['middle-name'] = 'Moses'
another_stooge.nickname = 'Moe'
// 原型关系是一种动态的关系。如果我们添加一个新的属性到原型中,该属性会立即对所有基于该原型创建的对象可见。
stooge.profession = 'actor'
another_stooge.profession // 'actor'
删除
delete 运算符可以用来删除对象的属性。如果对象包含该属性,那么该属性就会被移除。它不会触及原型链中的任何对象。
删除对象的属性可能会让来自原型链中的属性透现出来:
another_stooge.nickname // 'Moe'
// 删除 another_stooge 的 nickname 属性,从而暴露出原型的nickname属性
delete another_stooge.nickname
another_stooge.nickname // 'Curly'
减少全局变量污染
最小化使用全局变量的方法之一是为你的应用只创建一个唯一的全局变量。
第四章 函数
JavaScript设计得最出色的就是它的函数的实现,它几乎接近于完美。
函数用于代码复用、信息隐藏和组合调用,函数用于指定对象的行为。一般来说,所谓编程,就是将一组需求分解成一组函数与数据结构的技能。
函数对象
JavaScript中的函数就是对象。对象是“名/值”对的集合并拥有一个连接到原型对象的隐藏连接。对象字面量产生的对象连接到Object.prototype。函数对象连接到 Function.prototype(该原型对象本身连接到Object.prototype)。每个函数在创建时会附加两个隐藏属性:函数的上下文和实现函数行为的代码。
函数字面量
当没有给函数命名,就是匿名函数,如下例。
// 创建一个名为 add 的变量,并用来把两个数字相加的函数赋值给它
var add = function(a, b) {
return a + b
}
调用
除了声明时定义的形式参数,每个函数还接收两个附加的参数:
- this
- arguments
其中参数 this 在面向对象编程中非常重要,它的值取决于调用的模式。在 JavaScript 中一共有四种调用模式:
- 方法调用模式
- 函数调用模式
- 构造器调用模式
- apply 调用模式
其中,这些模式在如何初始化关键参数 this 上存在差异。
当实际参数(arguments)的个数与形式参数(parameters)的个数不匹配时,不会导致运行时错误。如果实际参数值过多了,超出的参数值会被忽略。如果实际参数值过少,缺失的值会被替换为 undefined。对参数值不会进行类型检查:任何类型的值都可以被传递给任何参数。
方法调用模式
当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this 被绑定到该对象。 this 到对象的绑定发生在调用的时候。这个“超级”延迟绑定(very late binding)使得函数可以对this高度复用。通过this可取得它们所属对象的上下文的方法称为公共方法(public method)
// 创建 my Object 对象。它有一个 value 属性和一个 increment 方法。
var myObject = {
value: 0,
increment: function(inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
}
myObject.increment()
document.writeln(myObject.value) // 1
myObject.increment(2)
document.writeln(myObject.value) // 3
函数调用模式
var add = add(3, 4)
以此模式调用函数时,this 被绑定到全局对象。这时语言设计上的一个错误。倘若语言设计正确,那么当内部函数被调用时,this 应该仍然绑定到外部函数的 this 变量。这个设计的后果就是方法不能利用内部函数来帮助它工作,因为内部函数的 this 被绑定到了错误的值,所以不能共享该方法对对象的访问权。解决方法:该方法定义一个变量并给它赋值为 this,那么内部函数就可以通过那个变量访问到 this。
myObject.double = function() {
var self = this // this 绑定到myObject
var helper = function() {
self.value = add(self.value, self.value) // 内部函数绑定到全局对象
}
helper()
}
myObject.double()
document.writeln(myObject.value) // 6
构造器调用模式
JavaScript 是一门基于原型继承的语言。这意味着对象可以直接从其他对象继承属性。该语言是无类型的。
当今大多数语言都是基于类的语言。尽管原型继承极富表现力,但它并未被广泛理解。JavaScript 本身对它原型的本质也缺乏信心,所以它提供了一套和基于类的语言类似的对象构建语法。
// 构造器调用模式
var Quo = function(string) {
this.status = string
}
Quo.prototype.get_status = function() {
return this.status
}
var myQuo = new Quo('confused')
document.writeln(myQuo.get_status())
一个函数,如果创建的母的就是希望结合 new 前缀来调用,那它就被称为构造器函数。按照约定,它们保存在以大写格式命名的变量里。如果调用构造器函数时没有在前面加上 new,可能会发生非常糟糕的事情,既没有编译时警告,也没有运行时警告,所以大写约定非常重要。
Apply 调用模式
// 《JavaScript 权威指南》中对 call() 和 apply() 的解释如下:
f.call(o)
f.apply(o)
// 每行代码和下面代码的功能类似(假设对象o中不存在名为m的属性)
o.m = f
o.m()
delete o.m
参数
当函数被调用时,会得到一个“免费”配送的参数,那就是arguments数组。函数可以通过此参数访问所有它被调用时传递给它的参数列表。这使得编写一个无须指定参数个数的函数称为可能:
var sum = function() {
var i, sum = 0
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i]
}
return sum
}
document.writeln(sum(4, 8, 15, 16, 23, 42))
因为语言的一个设计错误,arguments 并不是一个真正的数组。它只是一个“类似数组(array-like)”的对象。arguments 拥有一个 length 属性,但它没有任何数组的方法。
返回
一个函数总会返回一个值。如果没有指定返回值,则返回 undefined。
如果函数调用时在前面加上了 new 前缀,且返回值不是一个对象,则返回 this (该新对象)。
异常
throw 语句中断函数的执行。它应该跑出一个 exception 对象,该对象包含一个用来识别异常类型的 name 属性和一个描述性的 message 属性。你也可以添加其他的属性。
var add = function(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw {
name: 'TypeError',
message: 'add needs numbers',
}
}
return a + b
}
add(3, 6)
var try_it = function() {
try {
add('seven')
} catch(e) {
console.log(e.name + ': ' + e.message)
}
}
try_it()
如果在 try 代码块内抛出了一个异常,控制权就会跳转到它的 catch 从句。
扩充类型的功能
JavaScript 允许给语言的基本类型扩充功能。通过给 Object.prototype
添加方法,可以让该方法对所有对象都可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。
Function.prototype.method = function(name, func) {
this.prototype[name] = func
return this
}
Number.method('integer', function(){
return Math[this < 0 ? 'ceil' : 'floor'](this)
})
document.writeln((-10 / 3).integer())
作用域
JavaScript作用域为函数作用域,定义在函数中的参数和变量在函数外部是不可见的,而在一个函数内部任何位置定义的变量,在该函数内部任何地方都可见。
var foo = function() {
var a = 3
var b = 5
var bar = function() {
var b = 7
var c = 11
// 此时,a 为 3,b 为 7, c 为 11
a += b + c
// 此时,a 为 3,b 为 7, c 为 11
}
// 此时,a 为 3,b 为 5, c 没有定义
bar()
// 此时,a 为 21,b 为 5
}
闭包
下看两个闭包的例子:
var uniqueInteger = (function() { // 返回高级函数局部变量的例子
var counter = 0
return function() {
return counter++
}
}())
//返回一个包含两个方法的对象,这些方法继续享有访问 value 变量的特权
var myObject = (function(){
var value = 0
return {
increment: function(inc) {
value += typeof inc === 'number' ? inc : 1
},
getValue: function(){
return value
}
}
})
JS_权指: 函数对象可以通过作用域相互关联起来,函数体内的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”。
JS: Good Parts-函数可以访问被它创建时所处的上下文环境,这被称为闭包。
在同一个作用域链中定义两个闭包,这两个闭包共享同样的私有变量或变量。
function constfunc(v) {
return function() {
return v
}
}
var funcs = []
for (var i = 0; i < 10; i++) {
funcs[i] = constfunc(i)
}
funcs[5]() // => 5
下面的例子中,创建了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当 constfunc() 返回时,变量 i 的值是10,所有的闭包都共享这一个值,因此,数组中的函数的返回值都是同一个值。嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照(static snapshot)
所以,避免在循环中创建函数,它可能只会带来无谓的计算,还会引起混淆,正如下面这个糟糕的例子。
function constfuncs() {
var funcs = []
for (var i = 0; i < 10; i++) {
funcs[i] = function() {
return i
}
}
return funcs
}
funcs[5]() // => 10
回调
函数是的对不连续实践的处理变得更容易。例如,坚定有这么一个序列,由用户交互行为处罚,向服务器发送请求,最终显示服务器的响应。最自然的写法可能会是这样的:
request = prepare_the_request()
response = send_request_synchronously(request)
display(response)
这种方式的问题在于,网络上的同步请求会导致客户端进入假死状态。如果网络传输或服务器很慢,响应会慢到让人不可接受。
更好的方式是发起异步请求,提供一个当服务器的响应到达时随即触发的回调函数。异步函数立即返回,这样客户端就不会被阻塞。
request = prepare_the_request()
send_request_asynchronously(request, function(response) {
display(response)
})
模块
模块模式的一般形式是:一个定义了私有变量和函数的函数,利用闭包创建可以访问私有变量和函数的特权函数,最后返回这个特权函数,或者把它们保存到一个可访问到的地方。
使用模块模式就可以摒弃全局变量的使用,它促进了信息隐藏和其他优秀的设计实践。
级联
如果我们让方法返回 this 而不是 undefined ,就可以启用级联。
柯里化
var add = function(a, b) {
return a + b
}
var add1 = add.curry(1)
document.writeln(add1(6)) // 7
add1 是把 1 传递给 add 函数的 curry 方法后创建的一个函数。add1 函数把传递给它的参数的值加 1。
简单说,函数柯里化就是对高阶函数的降阶处理。
举个例子,就是把原本:
function(arg1,arg2)变成function(arg1)(arg2)
function(arg1,arg2,arg3)变成function(arg1)(arg2(arg3)
function(arg1,arg2,arg3,arg4)变成function(arg1)(arg2)(arg3)(arg4)……
记忆
var fibonacci = function() {
var memo = [0, 1]
var fib = function(n) {
var result = memo[n]
if (typeof result !== 'number') {
result = fib(n - 1) + fib(n - 2)
memo[n] = result
}
return result
}
return fib
}()
document.writeln(fibonacci(9))
第五章 继承
在基于类的语言中,对象是类的实例,并且类可以从另一个类继承。JavaScript 是一门基于原型的语言,这意味着对象直接从其他对象继承。
伪类
JavaScript 的原型存在诸多矛盾,它不直接让对象从其他对象继承,反而插入了一个多余的间接层:通过构造器函数产生对象。
当一个函数对象被创建时,Fucntion 构造器产生的函数对象会运行类似这样的一些代码:
this.prototype = {constructor: this}
新函数对象被赋予一个prototype属性,它的值时一个包含 constructor属性且属性值为该新函数的对象。这个 prototype 对象时存放继承特性的地方。因为 JavaScript 语言没有提供一种方法去确定哪个函数是大蒜用来做构造器的,所以每个函数都会得到一个 prototype 对象。constructor 属性没什么用,重要的是 prototype 对象。
var Mammal = function(name) {
this.name = name
}
Mammal.prototype.get_name = function() {
return this.name
}
Mammal.prototype.says = function() {
return this.saying || ''
}
var myMallal = new Mammal('Herb the Mammal')
var name = myMallal.get_name() // 'Herb the Mammal'
// 我们可以构造另一个伪类来继承 Mammal,
// 这是通过定义它的 constructor 函数并替换它的 prototype
// 为一个 Mammal 的实例来实现的。
var Cat = function(name) {
this.name = name
this.saying = 'meow'
}
// 替换 Cat.prototype 为一个新的 Mammal 实例
Cat.prototype = new Mammal()
// 扩充新原型对象,增加 purr 和 get_name 方法
Cat.prototype.purr = function(n) {
var i, s = ''
for (var i = 0; i < n; i++) {
if (s) {
s += '-'
}
s += 'r'
}
return s
}
Cat.prototype.get_name = function() {
return this.says() + ' ' + this.name + ' ' + this.says()
}
var myCat = new Cat('Henrietta')
var says = myCat.says() // 'meow'
var purr = myCat.purr(5) // 'r-r-r-r-r'
var name = myCat.get_name() // 'meow Henrietta meow'
上述做法的缺陷:
- 没有私有环境,所有的属性都是公开的
- 无法访问 super (父类)的方法
使用构造器函数存在一个严重的危害,如果你在调用构造器函数时忘记了在前面加上 new 前缀,那么 this 将不会被绑定到一个新对象上。悲剧的是, this 将被绑定到全局对象上,所以你不但没有扩充新对象,反而破坏了全局变量环境。
为了降低这个问题带来的风险,所有的构造器函数都约定命名称首字母大写的形式,并且不以首字母大写的形式拼写任何其他的东西。这样我们至少可以通过目视检查去发现是否缺少了 new 前缀。一个更好的备选方案就是根本不使用 new。
“伪类” 形式可以给不熟悉 JavaScript 的程序员提供便利,但它也隐藏了该语言的真实的本质。借鉴类的表示法可能误导程序员去编写过于深入与复杂的层次结构。许多复杂的类层次结构产生的原因就是静态类型检查的约束。JavaScript 完全摆脱了那些约束。在基于类的语言中,类继承是代码重用的唯一方式。而 JavaScript 有着更多且更好的选择。
对象说明符
构造器要接受一大串参数。这可能令人烦恼,因为要记住参数的顺序非常困难,所以在编写构造器时让它接受一个简单的对象说明符,就会更加友好:
var myObject = maker({
first: f,
middle: m,
last: l,
state: s,
city: c,
})
原型
在一个纯粹的原型类型中,我们会摒弃类,转而专注于对象。基于原型的继承相比基于类的继承在概念上更为简单:一个新对象可以继承一个旧对戏那个的属性。
// 原型
var myMammal = {
name: 'Herb the Mammal',
get_name: function() {
return this.name
},
says: function() {
return this.saying || ''
}
}
var myCat = Object.create(myMammal)
myCat.name = 'Henrietta'
myCat.saying = 'meow'
myCat.purr = function(n) {
var i, s = ''
for (i = 0; i < n; i += 1) {
if (s) {
s += '-'
}
s += 'r'
}
return s
}
myCat.get_name = function() {
return this.says + ' ' + this.name + ' ' + this.says
}
这是一种“差异化继承(differential inheritance)”,通过定制一个新的对象,我们指明它与所基于的基本对象的区别。
函数化
部件
第六章 数组
数组是一段线性分配的内存,它通过整数计算偏移并访问其中的元素。数组是一种性能出色的数据结构。不幸的是,JavaScript 没有像此类数组一样的数据结构。
作为替代,JavaScript 提供了一种拥有一些类数组(array-like)特性的对象。它把数组的下标转变称字符串,用其作为属性。它明显地比一个真正的数组慢,但它是用起来更加方便。它的属性的检索和更新的方式与对象一摸一样,只不过多一个可以用整数作为属性名的特性。
数组字面量
一个数组字面量是在一对方括号中包围零个或多个用逗号分隔的值的表达式。数组的第一个值将获得属性‘0’, 第二个值将获得属性名‘1’,依次类推:
// 数组
var empty = []
var numbers = [
'zero', 'one', 'two', 'three', 'four',
'five', 'six', 'seven', 'eight', 'nine'
]
empty[1] // undefined
numbers[1] // one
empty.length // 0
numbers.length // 10
var numbers_object = {
'0': 'zero',
'1': 'one',
'2': 'two',
'3': 'three',
'4': 'four',
'5': 'five',
'6': 'six',
'7': 'seven',
'8': 'eight',
'9': 'nine',
}
numbers 和 numbers_object 产生的结果相似。但是它们也有一些显著的不同。numbers 继承自 Array.prototype, 而 numbers_object 继承自 Object.prototype, 所以 numbers 继承了 大量有用的方法。同时,numbers 也有一个诡异的 length 属性,而 numbers_object 则没有。
在大多数语言中,一个数组的所有元素都要求是相同的类型。 JavaScript 允许数组包含人意混合类习惯的值:
var misc = [
'string', 98.6, true, false, null, undefined, ['nested', 'array'], {object: true}, NaN,
Infinity
]
misc.lenght // 10
长度
每个数组都有一个 length 属性,和大多数其他语言不同,JavaScript 数组的 length 是没有上界的。如果你用大于或等于当前 length 的数字作为下标来存储一个元素,那么 length 值会被增大以容纳新元素,不会发生数组越界错误。
length 属性的值是这个数组的最大整数属性名加上1。它不一定等于数组里的属性的个数:
var myArray = []
myArray.length // 0
myArray[1000000] = true
myArray.lenght // 1000001
// myArray 只包含一个属性
[] 后置下表运算符把它所含的表达式转换成一个字符串,如果该表达式有 toString 方法,就使用该方法的值。这个字符串将被用作属性名。
你可以直接设置 length 的值。设置更大的 length 不会给数组分配更多的空间。而把 length 设小将导致所有下标大于等于新 length 的属性被删除:
numbers.length = 3 // numbers 是 ['zero', 'one', 'two']
// 通过把下标指定为一个数组的当前 length,可以附加一个新元素到该数组的尾部
numbers[numbers.length] = 'shi' // numbers 是 ['zero', 'one', 'two', 'shi', 'go']
// 有时用 push 方法可以更方便地完成同样的事情:
numbers.push('go') // numbers 是 ['zero', 'one', 'two', 'shi', 'go']
删除
由于JavaScript 的数组其实就是对象,所以 delete 运算符可以用来从数组中移除元素:
delete numbers[2]
// numbers 是 ['zero', 'one', undefined, 'shi', 'go']
不幸的是,那样会在数组中留下一个空洞。这是因为排在被删除之后的元素保留着他们最初的属性。
可以用 splice 方法
numbers.splice(2, 1)
// numbers 是 ['zero', 'one', 'shi', 'go']
值为 'shi' 的属性的键值从 '3' 变到 '2' 。因为被删除属性后面的每个属性必须被移除,并且以一个新的键值重新插入,这对于大型数组来说可能会效率不高。
枚举
因为 JavaScript 的数组其实就是对象,所以 for in 语句可以用来遍历一个数组的所有属性。遗憾的是,for in 无法保证属性的顺序。采用常规的 for 语句可以避免这些问题:
var i
for (i = 0; i < myArray.length; i += 1) {
document.writeln(myArray[i])
}
容易混淆的发放
JavaScript 本身对于数组和对象的区别是混乱的。typeof 运算符报告数组的类型是 'object',这是没有意义的。
对他们的使用规则很简单: 当属性名是小而连续的整数时,你应该使用数组。否则,使用对象。
指定初始值
JavaScript 的数组通常不会预置值。如果你用 [] 得到一个新数组,它将是空的。如果你访问一个不存在的元素,得到的值则是 undefined。
第七章 正则表达式
JavaScript 的许多特性都借鉴自其他语言。语法借鉴自 Java,函数借鉴自 Scheme,原型继承借鉴自 Self。而 JavaScript 的正则表达式特性则借鉴自 Perl。
网友评论