若干年前记的 JavaScript 笔记,主要基于的标准应该是 ES5,但还是有一定的参考价值。编辑过程中稍微补充了一点点ES6(想到就随手补充一点)。
JavaScript 建立在一些非常优秀的想法和少数非常糟糕的想法之上。无可否认,JavaScript 是一门重要的语言,因为它是 Web 浏览器支持的少数语言之一。
JavaScript简介
JavaScript 是由 Web 发展初期的网景(Netscape)公司创建,“JavaScript” 是 Sun Microsystem 公司(现在的Oracle)的注册商标,用来特指网景(现在的Mozilla)对这门语言的实现。在JavaScript 还非常粗糙时,它就被集成到网景的 Navigator2 浏览器中。随着 Java 的小应用程序(Java applets)的失败,JavaScript 变成了默认的“Web语言”
网景将这门语言作为标准提交给了ECMA——欧洲计算机制造协会——由于商标上的冲突,这门语言的标准版本改了一个丑陋的名字“ECMAScript”。同样由于商标的冲突,微软对这门语言的实现版本取了一个广为人知的名字“Jscript”。实际上,几乎所有人都将这门语言叫做“JavaScript”。
JavaScript 建立在一些非常优秀的想法之上,如函数、弱类型、动态对象和富有表现力的对象字面量表示法(JSON的灵感)。但 JavaScript 建立在一个致命的想法之上:基于全局变量的编程模型。JavaScript 基于全局变量来进行连接,所有编译单元的所有顶级变量被撮合到一个被称为全局对象(the global object)的公共命名空间中。这是非常糟糕的,因为全局变量几乎是可以说是编程中避之唯恐不及的魔鬼,却是 JavaScipt 的基础。除此之外,JavaScript 还有不少“毒瘤”的语法,尽可能不要使用它们。
语法
注释
JavaScipt提供两种注释形式:一种是/* ... */
包围的块注释,另一种是//
开头的行注释。在JavaScript,建议避免使用/* ... */
,而用//
代替它。例如,
/*
var rm_a = /a*/.match(s);
*/
可以看到,在这个含有正则表达式的语句中,使用/* ... */
注释会导致一个错误。
标识符与保留字
JavaScript的标识符必须以字符、下划线(_
)或美元符号($
)开始。下面是合法的标识符:
i
my_variable_name
v13
_dummy
$str
JavaScript把一些标识符拿出来用做自己的关键字,因此不能在程序中使用以下的保留字:
break delete function
return typeof case
do if switch
var catch else
in this void
continue false instanceof
throw while debugger
finally new true
with defaule for
null try
在 ECMAScript 5 中还保留了这些关键字:
class const enum export extends import super
此外,下面的关键字在普通的 JavaScript 代码中是合法的,但是在严格模式下是保留字:
implements let private public yield
interface package protected static
上述保留字在 ES6 中很多都实现了对应的功能。
数字
JavaScript只有一个数字类型。它在内部被表示为64位的浮点数。因为,没有分离出整数类型,所以1和1.0的值相同。这完全避免了短整数的溢出问题。同样,100和1e2是相同的数字。
NaN
是一个数值,它表示不能产生正常结果的运算结果。NaN
不等于任何值,包括它自己。可以用isNaN(number)
检测NaN
。
Infinity
表示所有大于1.79769313476231570e+308的值
如果对计算机组成原理了解得比较多,应该可以想起来浮点数的IEEE 754标准。指数全1( 2 e − 1 2^e-1 2e−1),尾数为0,则是无穷
Infinity
;指数全1( 2 e − 1 2^e-1 2e−1),尾数非0,则是NAN
。
字符串
字符串可以被包含在一对单引号或双引号中。Unicode是一个16位的字符集,所以JavaScript中所有的字符都是16位的。 JavaScript没有字符类型,要表示一个字符,只需创建仅包含一个字符的字符串即可。
字符串是不可变的,一旦创建,就永远无法改变它。
两个包含完全相同的字符且字符顺序也相同的字符串被认为是相同的字符串。所以:
'c' + 'a' + 't' === 'cat'
字符串有一个length
属性。例如,"seven".length
是5。
字符串方法
除了length
属性,字符串还提供许多可以调用的方法:
var s = "hello, world";
s.charAt(0); // "h"
s.charAt(s.length - 1); // "d"
s.substring(1, 4); // "ell"
s.slice(1, 4); // "ell"
s.slice(-3); // "rld",最后三个字母
s.indexOf("l"); // 2,字符"l"首次出现的位置
s.lastIndexOf("l"); // 10,字符"l"最后一次出现的位置
s.split(", "); // ["hello", "world"]
s.replace("h", "H"); // "Hello, world",全文文字替换
s.toUpperCase(); // "HELLO, WORLD"
记住,在JavaScript中字符串是固定不变的,类似replace()
和toUpperCase()
都返回新的字符串,原字符串本身没有发生变化。
语句
var
被用在函数内部时,它定义的是这个函数的私有变量。
switch
、while
、for
和 do
语句允许有一个可选的前置标签(label),它配合 break
或者 continue
语句来使用。
mainloop: while (token != null) {
// 忽略这里的代码...
continue mainloop; // 跳转到下一次循环
// 忽略这里的代码...
}
continue
和break
是唯一可以使用语句标签的语句。
代码块是一对花括号中的一组语句。JavaScript 中的代码块不会创建新的作用域,因此变量应该定义在函数的头部,而不是在代码块中。JavaScript没有块级作用域。
下面的值为假(falsy)
false
null
undefined
- 空字符串
''
- 数字
0
- 数字
NaN
case
表达式的值不一定必须是常量。
JavaScript的 %
是通常数学意义上的模运算,实际上是“求余”运算。两个运算数都是正数时,求模运算和求余运算的值相同。两个运算数中存在负数时,求模运算和求余运算的值则不相同。
表达式
属性访问表达式
JavaScript为属性访问定义了两种语法:
expression.identifier
expression[expression]
具体例子:
var o = {x : 1, y : {z : 3}};
var a = [o, 4, [5, 6]];
o.x // 1
o.y.z // 3
o["x"] // 1
a[1] // 4
a[2]["1"] // 6
a[0].x // 1
不管使用哪种形式的属性访问表达式,在.
和[
之前的表达式都会首先计算。如果计算结果是null
和undefined
,表达式会抛出一个类型错误异常。
显然,.identifier
的写法更加简单,应该优先考虑,它更加紧凑且可读性更好。
如果属性名称是一个保留字或者包含空格和标点符号,或是一个数字(对于数组而言),则必须使用方括号的写法。
对象
JavaScript的简单类型包括数字、字符串、布尔值(true
和false
)、null
值和undefined
值,其它的所有值都是对象。数字、字符串和布尔值“类似”对象,它们也拥有方法,但它们是不可变的。
JavaScript中的对象是可变的键控集合(keyed collections)。在JavaScript中,数组是对象,函数是对象,正则表达式是对象,当然,对象自然是对象。
创建和访问
一个对象字面量就是包围在一对花括号中的零或多个名/值对。
var empty_object = {};
var stooge = {
"first-name": "Jerome",
"last-name": "Howard"
};
如果属性名是一个合法的JavaScript的标识符且不是保留字,则并不要求用""
括住。所以用"first-name"
是必须,但是first_name
可以不用""
。【first_name是合法的标识名,first-name不是】
要检索对象里包含的值,可以使用点.
语法或者[]
语法。如果你尝试检索一个并不存在的成员属性的值,将返回 undefined
。
stooge["first-name"] // "Jerome"
stooge["FIRST-NAME"] // undefined
||
运算符可以用来填充默认值:
var middle = stooge["middle-name"] || "(none)";
尝试从undefined
的成员属性中取值将会导致TypeError
异常,可以使用&&
避免错误。
更新值的语法也没有太大的意外:
stooge['first-name'] = 'Jerome';
如果对象之前没有拥有那个属性名,那么该属性就被扩充到对象中。
引用
对象通过引用来传递。它们永远不会被复制:
var x = stooge;
x["first-name"] = 'Curly';
// 因为x和stooge是指向同一个对象的引用,所以first_name为'Curly'
var first_name = stooge["first-name"];
// a, b和c都引用一个不同的空对象
var a = {}, b = {}, c = {};
// 引用同一个空对象
a = b = c = {};
原型
每个对象都连接到一个原型对象,并且它可以从中继承属性。通过对象字面量创建的对象都连接到 Object.prototype
,它是JavaScript中标配对象。
原型连接只有在检索值的时候才被用到。如果我们尝试去获取对象的属性值,但该对象没有此属性名,那么 JavaScript 会试着从原型对象中获取属性值。 如果那个原型对象也没有该属性,那么再从它的原型中寻找,以此类推。直到该过程最后到达终点Object.prototype
。
如果想要的属性完全不存在于原型链中,那么结果就是undefined
值。这个过程称为委托。
反射
检查对象并确定对象有什么属性是很容易的事情,只要试着去检索该属性并验证取得的值。
typeof
操作符对确定属性的类型很有帮助:
typeof flight.number // 'number'
typeof flight.toString // 'function'
当你想让对象运行时动态获取自身信息时,你关注的更多是数据,而你应该意识到一些值可能会是函数。
另一个方法是使用hasOwnProperty
方法,如果对象拥有独有的属性,它将返回true
。hasOwnProperty
方法不会检查原型链。
flight.hasOwnProperty('number') // true
flight.hasOwnProperty('constructor') // false
枚举
for in
语句可以用来遍历一个对象中的所有的属性名。枚举的过程会列出所有的属性,包括函数和你可能不关心的原型中的属性。所以,有必要过滤掉那些你不想要的值,最常用的过滤器是hasOwnProperty
方法配合typeof
来排除函数:
var name;
for (name in another_stooge) {
if (typeof another_stooge[name] !== 'function') {
document.writeln(name + ': ' + another_stooge[name]);
}
}
属性名出现的顺序是不确定的,如果你要确保顺序,最好的办法是创建一个数组:
var i;
var properties = [
'first-name',
'middle-name',
'last-name',
'profession'
];
for (i = 0; i < properties.length; i += 1) {
document.writeln(properties[i] + ': ' + another_stooge[properties[i]]);
}
删除
delete
可以用来删除对象的属性,它不会触及原型链中的任何对象。
删除对象的属性可能会让来自原型链中的属性透视出来:
another_stooge.nickname // 'Moe'
// 删除another_stooge的nickname属性,从而暴露出原型的nickname属性
delete another_stooge.nickname;
another_stooge.nickname // 'Curly'
减少全局变量污染
全局变量削弱了程序的灵活性,应该避免使用。最小化使用全局变量的方法之一是为你的应用只创建一个唯一的全局变量:
var MYAPP = {};
// 该变量成为你的应用的容器
MYAPP.stooge = {
"first-name": "Joe",
"last-name": "Howard"
};
MYAPP.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"
}
};
把全局的资源纳入一个名称空间之下,程序与其他应用程序、组件或类库之间发生冲突的可能性就会显著降低。程序也将更容易阅读,因为很明显,MYAPP.stooge
指向的是顶层结构。
还可以利用闭包来进行信息隐藏的方方式来减少全局变量污染,它是另一种有效减少全局污染的方法。
“毒瘤”特性(ES6已经有很大改善了)
JavaScript 一些难以避免的问题,必须知道并且做好应对措施。
全局变量
在JavaScript所有的糟糕特性中,最糟糕的一个就是它对全局变量的依赖。全局变量在微型程序中可能会带来方便,但随着程序越来越大,很快就难以管理。因为一个全局变量可以被程序的任何部分在任意时间修改,它们使得程序的行为变得极度复杂。在程序中使用全局变量降低了程序的可靠性。
许多编程语言都有全局变量,例如:Java中的 public static
成员属性就是全局变量。JavaScript的问题不仅在它允许使用全局变量,而且在于它依赖全局变量。
共有三种方式定义全局变量,第1种是在任何函数之外放置一个var
语句:
var foo = value;
第2种是直接给全局对象添加一个属性。全局对象是所有全局变量的容器。在Web浏览器里,全局对象名为window
:
window.foo = value;
第3种,是直接使用未经声明的变量,这被称为隐式的全局变量:
foo = value;
(ES6)块级作用域变量
-
使用
let
关键字创建块级作用域变量:let ninja = "旗木卡卡西"
-
const
关键字创建块级作用域常量,创建后不能再更改:const ninja = "大蛇丸"
作用域
JavaScript来源于C,也有花括号,却**没有块级作用域( ** (ES6引入了 let
和 const
,有了块级作用域)。在大多数语言中,声明变量的最好地方是在第一次用到它的地方。但这种做法在JavaScript里反而是一个坏习惯,因为它没有块级作用域。更好的方式是在每个函数的开头部分声明所有变量。
糟粕特性
JavaScript存在一些有问题的特性,不过很容易就可以避免使用它。
==
如果两个操作数是不同的类型,==
会试图是去强制转换值的类型。规则复杂且难以记忆:
'' == '0' // false
0 == '' // true
0 == '0' // true
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
' \t\r\n ' == 0 // true
请始终使用===
和!==
,如果上面的所有比较使用===
,结果都是false
。
==
在某种程度上违背了传递性。
with语句
with
本来时用来快速访问对象的属性。但它的结果可能是不可预料的,所以应该避免使用它。
下面的语句:
with (obj) {
a == b;
}
和下面的代码做的是同样的事情:
if (obj.a === undefined) {
a = obj.b === undefined ? b : obj.b;
} else {
obj.a = obj.b === undefined ? b : obj.b;
}
通过阅读代码,你不可能辨别出你会得到的是这些语句中的哪一条。with
语言的存在,本身就影响了JavaScript处理器的速度,因为它阻断了变量名的词法作用域绑定。
eval
如果你知道点表示法,但不知道下标表示法,就可能会这样写:
eval("myValue = myObject." + mykey + ";");
而不是:
myvalue = myObject[mykey];
使用eval
形式的代码难以阅读,而且性能显著降低。除此之外,eval
函数还减弱了你的应用程序的安全性,它赋予了被求值的文本太多的权力。
new
如果忘记了使用new
运算符,你得到的就是一个普通的函数调用,并且this
被绑定到全局对象,而不是新创建的对象。这意味着当你的函数尝试去初始化新成员属性时它将会污染全局变量,而且没有编译和运行都没有警告。
一个更好的应对策略就是根本不使用new
,也包括避免使用new Object
和new Array
,可以使用{}
和[]
代替。
void
在很多语言中,void
是一种类型,表示没有值。在JavaScript中,void
是运算符,接受一个运算数并返回undefined
。没有什么用。