终于开始读蝴蝶书,记录一下阅读中的体会
第一章 精华
每种语言都有精华和鸡肋,但是标准委员会不会移除语言中鸡肋的部分,因为这样会损害所有依赖鸡肋部分的程序。
但是,我们可以定义自己的子集,基于精华部分去编写更好的程序。
为什么使用JavaScript
它与浏览器结合使它成为世界上最流行的编程语言,但浏览器的API和DOM相当糟糕,导致处理DOM是一件痛苦的事情,因为它的规范制定得很拙劣且实现相互不一致。所以本书只讲Javascript语言本身,并不涉及DOM。
关于DOM可以看《JavaScript DOM编程艺术》
第二章 语法
本章介绍Javascript的精华部分的语法,并简要概述语言结构。
标识符
标识符由一个字母开头,其后可选择性地加上一个或多个字母、数字或下划线。标识符不能使用下面这些保留字:
abstract
boolean break byte
case catch char class const continue
debugger default delete do double
else enum export extends
false final finally float for function
goto
if implements import in instanceof int interface
long
native new null
package private protected public
return
short static super switch synchronized
this throw throws transient true try typeof
var volatile void
while with
JavaScript不允许使用保留字来命名变量或参数,也不允许在对象字面量中,或者在一个属性存取表达式的点号之后,使用保留字作为对象的属性名。
数字
Javascript只有单一的数字类型,它在内部被表示为64位的浮点数。和大多数语言不同,它没有区分整数和浮点数。
-
指数
『e之前的部分乘以10的e之后部分的次方』,如1e2
-> 100 -
负数
-
前缀构成 -
NaN
- 是一个数值,表示一个不能产生正常结果的运算结果
- NaN不等于任何值,包括它自己
- 可以用
isNaN(number)
检测 NaN
-
Infinity
表示所有大于1.79769313486231570e308
的值
字符串
字符串可以被包围在
'
或"
中,可能包含0个或多个字符。\
是转义符。JavaScript没有字符类型,要表示一个字符,创建一个包含一个字符的字符串就行。
-
字符串可以通过
+
连接其他字符串'a'+'b'+'c' === 'abc' //true
字符串有
length
属性
语句
一个编译单元包含一组可执行的语句,浏览器中每个 <script>
都提供一个被编译且立即执行的编译单元。因为缺少链接器,JavaScript把它们一起抛入一个公共的全局对象(web 中这个全局对象是 window
)中。
链接器:一个程序,将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件。
简单地说,链接器的工作就是解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。
代码块是包在一对花括号中的一组语句。不像其他语言,JavaScript中的代码块不会创建一个新的作用域。
表达式
最简单的表达式:
- 字面量值:比如字符串或数字
- 变量
- 内置的值:true、false、null、undefined、NaN、Infinity
- 以
new
为前导的调用表达式 - 以
delete
前导的属性存取表达式 - 包在圆括号中的表达式
- 以一个前缀运算符作为前导的表达式
- 表达式后面跟着:
- 一个插入运算符与另一个表达式
- 三元运算符
?
后跟着另一个表达式,之后:
再接着第三个表达式 - 一个函数调用
- 一个属性存取表达式
第三章 对象
JavaScript简单类型有数字、字符串、布尔值、null、undefined,其他所有的值都是对象(数组、函数、正则表达式都是对象)。
数字、字符串、布尔值虽然拥有方法(包装对象),但并不是对象。
包装对象:
每当读取一个基本类型值的时候,后台会创建一个对象的基本包装类型的对象,从而能够调用一些方法来操作这些数据。
var s1 = 'abcdefg' ;
var s2 = s1.substring(2) ;
后台自动完成下列处理:
- 创建String类型的一个实例
- 在实例上调用指定的方法
- 销毁这个实例
所以上面代码等同于:
var s1 = new String('abcdefg') ;
var s2 = s1.substring(2) ;
s1 = null ;
对象字面量
var flight = {
airline: "Oceanic",
number: 815,
departure: {
IATAL: "SYD",
time: "2004-09-22 14:55",
city: "Sydney"
},
arrival: {
IATA: "LAX",
time: "2004-09-23 10:42",
city: "Los Angeles"
}
}
检索
-
[]
:flight['number']
-
.
:flight.number
更新
通过赋值语句更新,如果属性名已经存在于对象中,则被替换;如果对象中没有那个属性名,则添加。
stooge['first-name'] = 'Jerome'
引用
对象通过引用来传递,它们永远不会被拷贝。
var a = {
name: 'a'
}
var b = a
b.name = 'b'
console.log(a.name) // b
这里牵扯出 JavaScript 深拷贝和浅拷贝的问题
上例是浅拷贝深拷贝见下:
var deepCopy= function(source) {
var result={};
for (var key in source) {
result[key] = typeof source[key]===’object’? deepCoyp(source[key]): source[key];
}
return result;
}
此时
var b = deepCopy(a)
得到的b
就和a
没有引用关系,即修改b
不会影响a
了
原型
每个对象都连接到一个原型对象,并且从中继承属性。所有通过对象字面量创建的对象都连接到 Object.prototype
这个JavaScript中标准的对象。
创建一个对象时,可以选择某个对象作为它的原型:
var o = {o1:1,o2:function(){alert(1)}}
function F(){}
F.prototype = o
var f = new F()
反射
使用 hasOwnProperty
检查属性是否是对象独有的,它并不会检查原型链。
flight.hasOwnProperty('number'); //true
枚举
for in
可以遍历对象中所有的属性名(见深拷贝部分)
删除
delete
可以删除对象属性,不会触及原型链中的任何对象
减少全局变量污染
最小化使用全局变量的一个方法是创建唯一一个全局变量:
var App = {}
App.stooge = {
"first-name": "Joe",
"last-name": "Howard"
}
App.flight = {
airline: "Oceanic",
number: 815
}
减少全局变量污染另一个办法是使用闭包进行信息隐藏
第四章 函数
函数包含一组语句,是Javascript的基础模块单元,用于代码复用、信息隐藏和组合调用。
一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。
函数对象
函数就是对象,对象是键值对的集合并且拥有一个连到原型对象的隐藏连接。对象字面量产生的对象连接到 Object.prototype
,函数对象连接到 Function.prototype
(该原型对象本身又连接到 Object.prototype)。
每个函数在创建时附有两个附加的隐藏属性:
- 函数上下文
- 实现函数行为的代码
每个函数对象在创建时也会带一个 prototype
属性,它的值是一个拥有 constructor
属性且值为该函数的对象。
函数字面量
函数对象可以通过函数字面量来创建:
var add = function(a,b){
return a + b;
}
函数字面量包含四部分:
- 保留字
function
- 函数名,可以被省略(匿名函数)
- 参数,逗号分隔
- 花括号中的语句
函数字面量允许出现在任何允许表达式出现的地方,函数也可以被定义在其他函数中。一个内部函数可以访问自己的参数和变量,同时它也能方便地访问它被嵌套在其中的那个函数的参数和变量。通过函数字面量创建的函数对象包含一个连到外部上下文的连接,被称为闭包。
调用
函数在调用的时候有两个附加参数:this
、arguments
。
this
是调用上下文,值取决于函数调用的模式。
1.方法调用模式
一个函数被保存为对象的一个属性时,即为一个方法。当一个方法被调用时,this
被绑定到该对象。
var dog = {
name : 'xxx' ,
leg:{
sum : 4 ,
move:function(){
console.log(this) ; //Object,是leg对象,而不是dog对象,下面证明了
alert(this.name) ; //underfined
alert(this.sum) ; //4
}
}
}
dog.leg.move();
2.函数调用模式
函数仅仅当做函数来调用时,this
被绑定到全局对象。
var a = 111 ;
function t1(){
var a = 1
function t2(){
console.log(this.a) //111,这其实很不合理,应该指向t2的。
}
t2()
}
t1()
这其实是语言设计上的一个错误,倘若语言设计正确,当内部函数被调用时,this
应该仍然绑定到外部函数的 this
变量。
3.构造器调用模式
如果一个函数前面带上 new 调用,那么将创建一个隐藏连接到该函数的 prototype
成员的新对象,同时 this 将被绑定到那个新对象上。
function Dog(name){
this.name = name ;
}
Dog.prototype.cry = function(){
alert(this.name)
}
var dog1 = new Dog('xxx');
dog1.cry(); // 'xxx'
4.Apply/Call调用模式
apply 接受两个参数,第一个是将被绑定给this的值,第二个就是一个参数数组。
call 与 apply 相同,不过第二个参数不是数组。
var dog = {
leg : 4 ,
color:'yellow'
}
var color = 'red' ;
function t(){
alert(this.color) ;
}
t(); // red , 因为指向this在函数中调用指向window
t.call(dog); //yellow , 把t()的作用域指向了dog
再来说说 arguments
,它是一个类数组对象(拥有length属性,但缺少所有数组方法)。通过它可以访问函数调用时传递给函数的参数列表。
返回
一个函数调用时,将暂停当前函数的执行,传递控制权和参数给新函数。它从第一个语句开始执行,并在遇到关闭函数体的 }
时结束。使得函数把控制权交还给调用该函数的程序部分。
return
语句可用来使函数提前返回,当 return 执行时,函数立即返回而不再执行余下的语句。一个函数总是会返回一个值,如果没有指定返回值,则返回 undefined
。
如果函数前加上 new
来调用,且返回值不是一个对象,则返回this(该新对象)。
异常
抛出异常
function add(a,b){
if(typeof a!=='number' || typeof b!=='number'){
throw {
name: 'TypeError',
message: 'add needs numbers'
}
}
return a + b;
}
throw
语句中断函数的执行,抛出一个 exception
对象,该对象包含可识别异常类型的 name
属性和一个描述性的 message
属性。
该 exception
对象将被传递到一个 try
语句的 catch
从句:
try{
add('seven')
}catch(e){
console.log(e.name)
console.log(e.message)
}
如果在 try
代码块中抛出异常,控制权就跳转到其 catch
从句。
给类型增加方法
Number.prototype.integer = function(){
return Math[this < 0 ? 'ceiling' : 'floor'](this) //this指向实例
}
var num = 10/3
console.log(num.integer()) ; // 3
作用域
作用域空间那个值变量和参数的可见性和生命周期,对程序员来说很重要,因为它减少了命名冲突,并且提供了自动内存管理。
JavaScript没有块级作用域,却有函数作用域:定义在函数中的参数和变量在函数外部是不可见的,而且在一个函数中的任何位置定义的变量在该函数中任何地方都可见。
下面这个例子与以对象字面量初始化对象不同,通过调用一个函数形式去初始化对象,返回一个对象字面量。此函数定义了一个 val
变量,该变量对 addVal
和 getVal
总是可用的,但函数的作用域使得其对其他程序来说是不可见的。
// 从设计模式的角度来说这是模块模式
var o = (function(){
var val = 0;
return {
addVal: function(){
val += 1
},
getVal: function(){
console.log(val)
}
}
})()
联想到之前我做的一个小游戏,是20秒内完成任务,使用
restTime
做倒计时变量。后来同事把restTime修改了,成绩贼高。最后我就是用这种办法把restTime
像val
一样隐藏了起来。
闭包
作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。
var o = function(){
var val = 0;
return {
addVal: function(){
val += 1
},
getVal: function(){
console.log(val)
}
}
}
var oo = o()
oo.addVal()
oo.addVal()
oo.getVal() // 2
当调用 o
时,返回一个包含addVal
和getVal
的新对象,该对象的引用保存在 oo
中。虽然 o
返回了,但是 oo
的addVal
和getVal
有访问 val
的特权,它们可以访问被创建时所处的上下文,这就是闭包。
模块
可以使用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。
具体见『作用域』部分-模块模式
第五章 继承
当一个函数对象被创建时,Function 构造器产生的函数对象会运行类似这样的代码:
this.prototype = {constructor: this}
每个函数都会得到一个 prototype
对象,其值是包含一个 constructor
属性且属性值为该新函数对象。该 prototype
对象是存放继承特征的地方。
第六章 数组
区分数组和对象
一个常见的错误是在须使用数组时使用了对象,在须使用对象时使用了数组。其实规则很简单:当属性名是小而连续的整数时,用数组,否则就用对象。
JavaScript中数组 typeof
返回是 object
,这并不能区分数组和对象。
var is_array = function(value){
return value &&
typeof value === 'object' &&
typeof value.length === 'number' &&
typeof value.splice === 'function' &&
!(value.propertyIsEnumerable('length'))
}
- 首先,我们判断这个值是否为真,我们不接受
null
和其他为假的值。 - 其次,我们判断这个值的
typeof
运算结果是否是object
。对于对象、数组和null
来说,将得到true
。 - 第三,我们判断这个值是否有一个值为数字的
length
属性,对于数组是true
,对于对象则为false
- 第四,判断这个值是否包含一个
splice
方法。对于数组,返回true
- 最后,我们判断
length
属性是否是可枚举的
这真的很复杂,实际上,我一直是这样用的,什么类型都能检测,堪称万能:
var toString = Object.prototype.toString
function isObject(obj) {
return toString.call(obj) === "[object Object]"
}
function isString(obj) {
return toString.call(obj) === "[object String]"
}
function isArray(obj) {
return toString.call(obj) === "[object Array]"
}
function isFunction(obj) {
return toString.call(obj) === "[object Function]"
}
第七章 正则表达式
没说什么,都是基本语法,略过
第八章 方法
Array
concat(item...)
返回一个新数组,并不会修改原数组
var a1 = [2,3]
var a2 = [332,12]
console.log( a1.concat(a2) ); //[ 2, 3, 332, 12 ]
join(separator)
把一个 array 构造成一个字符串,并用 separator
作为分隔符把它们连接在一起。
pop(item...)
移除数组中最后一个元素并返回该元素
push(item...)
将一个或多个元素添加到数组尾部,会修改原数组
reverse()
反转数组中元素的顺序,会修改原数组,返回当前数组
shift()
移除数组中第一个元素
slice(start,end)
从start开始,到end为止(不包括end,可选,默认值是length)复制数组
sort(comparefn)
对数组中的内容排序,并不能给数字排序,因为默认比较函数是假定要被排序的元素都是字符串。
比较函数接受两个参数,并且如果两个参数相等返回0,如果第一个参数应该排在前面,则返回一个负数,如果第二个参数应该排在前面,则返回一个正数。
// a 比 b小时返回 -1,而且根据上述规则 a 会排在前面
// 所以这是从小到大的排序
var arr = [1,123,341,34,123]
arr.sort(function(a,b){
if(a==b){
return 0
}else{
return a < b ? -1 : 1
}
})
splice(start,deleteCount,item...)
splice 从数组中移除一个或多个元素,并用新的item代替他们。
start是从数组中移除元素的开始位置,deleteCount是要移除的个数。
会修改原数组,返回一个包含被移除元素的数组。
var arr = [23,3,23,2]
var b = arr.splice(0,1)
console.log(arr) ; //[ 3, 23, 2 ]
console.log(b) ; //[ 23 ]
deleteCount 为0时,则为添加新元素:
var arr = [23,3,23,2]
var b = arr.splice(1,0,'aa')
console.log(arr) // [ 23, 'aa', 3, 23, 2 ]
console.log(b) // []
deleteCount 与 item的个数相等时,则为替换:
var arr = [23,3,23,2]
var b = arr.splice(1,1,'aa')
console.log(arr) //[ 23, 'aa', 23, 2 ]
console.log(b) //[ 3 ]
unshift(item...)
将item从数组头部插入数组
Function
apply(thisArg,argArray)
见 『Apply/Call调用模式』
Number
toFixed(fractionDigits)
把这个 number 转换成一个十进制形式的字符串。可选参数 fractionDigits
控制其小数点后的数字位数。
Math.PI.toFixed(); //3
Math.PI.toFixed(2); //3.14
Math.PI.toFixed(4); //3.1415
toPrecision(precision)
同 toFixed
,参数控制有效数字的位数
toString()
将number转换成字符串
Object
hasOwnProperty(name)
只检查此对象中的属性,原型链中得同名属性不会被检查。如果存在此属性则返回 true
。
String
charAt(pos)
返回在字符串中pos处的字符
charCodeAt(pos)
返回不是一个字符串,而是以整数形式表示的字符码位
concat(string...)
与其他字符串连接起来构造一个新字符串,不常用,因为 +
也能满足需求
indexOf(searchString,pos)
在字符串内查找另一个字符串 searchString,如果被找到,则返回第一个匹配字符的位置,否则返回 -1 。
可选参数 pos 设置从字符串的某个指定位置开始查找。
lastIndexOf(searchString,pos)
与indexOf类似,不同从末尾开始查找
slice(start,end)
复制字符串的一部分构造一个新的字符串
split(separator,limit)
把字符串分割成片段创建数组,limit可限制被分割的片段数量。
一个有意思的技巧:
new Array(10).join('0').split('')
生成10个元素为0的数组
toLowerCase()
将字符串中所有字母转化为小写
toUpperCase()
将字符串中所有字母转化为大写
第九章 代码风格
其他都略去,作者提到『用一个单独的全局变量去包含一个脚本应用或工具库,每个工具库都有它自己的命名空间,所以我很容易使用对象去管理代码』。这个思想在YAHOO的YUI库中得到彻底的贯彻,在YUI中仅用到两个全局变量:YAHOO
和YAHOO_config
第十章 优美的特性
精简的JS里都是好东西:
- 函数是头等对象
- 基于原型继承的动态对象:对象是无类别的,我们可以通过普通的赋值给任何对象增加一个新成员元素,一个对象可以从另一个对象继承成员元素
- 对象字面量和数组字面量:这对创建新对象的数组来说是十分方便的方法。JavaScript字面量是数据交换格式JSON的灵感之源。
附录 糟粕
这一部分用来吐槽JS这门语言设计上不周到的地方
全局变量
共三种方法定义全局变量:
- 脱离任何函数var语句
var foo = value
- 直接添加一个属性到全局对象中,全局对象是所有全局变量的容器。在web中,全局对象是
window
:window.foo = value
- 使用未声明的变量,这被称为隐式的全局变量:
foo = value
之前说过,可以通过 创建一个全局变量 和 闭包 减少全局变量污染(注意,只是减少,没办法避免,总要有暴露出来的变量,不要钻牛角尖)。
作用域
没有块级作用域,只有函数作用域
自动插入分号
JavaScript 有一个机制,会试图通过自动插入分号来修正有缺损的程序。它有可能会掩盖更为严重的错误。
return
{
status: true
}
看起来是返回一个对象,但是自动插入分号让它返回了undefined
,这样可以避免:
return {
status: true
}
typeof
typeof并不能正确地检测数据类型:
typeof null ; //object
所以使用 Object.prototype.toString.call(null)
这个办法就好,万能的!
附录 JSLint
JSLint,一个JavaScript语法检查器和校验器,它取得源文本并进行扫描。如果发现问题,会返回一个消息。
有人认为JSLint过于严格,就有了jshint
我的总结
关于作者
这本书的作者是Douglas Crockford,是 JSON
、JSLint
的发明者,ECMA JavaScript2.0标准化委员会委员,曾任Yahoo!资深JavaScript架构师,现任PayPal高级JavaScript架构师。
关于这本书
这本书被称为『蝴蝶书』,是JS的进阶书籍。我在很早之前看过一次,这两天再看,体会又不同了。因为我记性不是很好,所以赶紧记录一下读书笔记。如果以后再读,到时候也可以把这篇文章拿出来对比一下。这篇文章记录了我不是很熟东西,一些我认为不是很重要的没有记录,还用注释的方式加入了一些我的笔记。
这本书在知乎上的讨论