总览
JavaScript 是一门多范式的,有着类型、操作符、标准内置对象和方法的动态语言。它的语法基于 C 和 Java。JavaScript 通过对象原型支持面向对象编程,而不是类(对象原型在创建对象前不需要提前定义类,这是它和面向对象最大的区别)。JavaScript 同时支持函数式编程,因为一切皆对象,函数也可以存储在变量中并像其它对象那样被传递。
JavaScript 中的类型包括:
- Number
- String
- Boolean
- Symbol(new in 2015)
- Object
- Function
- Array
- Date
- RegExp
- null
- undefined
数值
在 JavaScript 中的数字按照 "double-precision 64-bit format IEEE 754 values"标准。也就是说,JavaScript 中没有整数表示。
你可以使用内置的parseInt()
将字符串转化为整数。其中第二个参数用来指明转成几进制。
parseInt('123', 10); // 123
parseInt('010', 10); // 10
同样地,你可以使用parseFloat()
转换成浮点值。
使用+
操作符也可以转化字符串到数字:
+ '42'; // 42
+ '010'; // 10
+ '0x10'; // 16
当对不存在的数值进行转换时,会返回 NaN
。NaN
与任何数字的操作都会返回NaN
。可以使用函数isNaN()
来判断一个变量是否是NaN
。
JavaScript 中还有特殊值Infinity and -Infinity
,同样可以使用内置方法isFinite()
测试该值。
字符串
在 JavaScript 中的字符串是 Unicode 字符序列。更准确地,它们是 UTF-16的码元序列。每一个码元表示一个十六进制数。每一个字符可通过一或两个码元表示。
其他类型
JavaScript 区分了null
(它是一个故意被赋为空的值)和undefined
(未初始化的值)。在 JavaScript 中在不赋值的情况下声明变量是合法的。如果你这么做了,变量的类型是undefined
,undefined
实际上是一个常量。
JavaScript 也有布尔值。任何值都可以根据以下规则转化为布尔值:
false
,0
,""
,NaN
,null
,undefined
都可以转化为false
。- 所有其他值都可以转化为
true
。
你也可以显式地执行下列转化:
Boolean(''); // false
Boolean(234); // true
大部分情况下,JavaScript 会自动进行布尔值的转化,例如在if
语句中。
变量
JavaScript 中的变量可以通过三个关键字之一来进行声明:let
,const
,var
。
let
允许你声明块级变量。被声明的变量在其被包裹的代码块上下文中有效。
const
允许你声明只读变量。该变量在其被声明的上下文中是可用的。
const Pi = 3.14; // variable Pi is set
Pi = 1; // will throw an error because you cannot change a constant variable.
var
是最常用的声明关键字。它并没有其它两个关键字的约束。使用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
JavaScript 和其他语言(Java)一个重要的区别在于,JavaScript 没有块作用域。直到 ECMAScript2015,let
和const
声明允许你创建块级变量。
操作符
JavaScript 中的数值操作符是+
,-
,*
,/
,%
。通过=
进行赋值操作。也可以使用复合的赋值语句,例如:+=
,-=
。
也可以使用++
和--
来自增和自减。
+
也可以用来连接字符串。
'3' + 4 + 5; // "345"
3 + 4 + '5'; // "75"
任何数值和字符串相加都会转化成字符串。
JavaScript 中的比较操作符是<
,>
,<=
,>=
。可同时在字符串和数值间比较。两个等号的比值操作只会比较值而不比较类型,三个等号的比值操作既会比较类型也会比较值。
123 == '123'; // true
1 == true; // true
123 === '123'; // false
1 === true; // false
控制结构
条件结构:
var name = 'kittens';
if (name == 'puppies') {
name += ' woof';
} else if (name == 'kittens') {
name += ' meow';
} else {
name += '!';
}
name == 'kittens meow';
循环结构:
while (true) {
// an infinite loop!
}
var input;
do {
input = get_input();
} while (inputIsNotValid(input));
for (var i = 0; i < 5; i++) {
// Will execute 5 times
}
for (let value of array) {
// do something with value
}
for (let property in object) {
// do something with object property
}
与或操作:
var name = o && o.getName();//if the first part is false, the last will not execute
var name = cachedName || (cachedName = getName()); //if the first part is true, the last will not execute
三元操作符:
var allowed = (age > 18) ? 'yes' : 'no';
switch 语句:
switch (action) {
case 'draw':
drawIt();
break;
case 'eat':
eatIt();
break;
default:
doNothing();
}
对象
JavaScript 中的对象可以简单地看做键值对的集合,和下列数据结构类似:
- python 中的 Dictionaries
- Perl 和 Ruby 中的 Hashes
- C 和 C++中的 Hash tables
- Java 中的 HashMap
- PHP 中的 Associative arrays
在 JavaScript 中一切皆对象,因此 JavaScript 程序涉及大量哈希表查找。
对象的“键”是 JavaScript 字符串,“值”可以是任何 JavaScript 值。这允许你构建任意复杂度的对象。
有两种基本方式创建空对象:
var obj = new Object();
var obj = {};
它们在语义上是等价的。第二种方式是对象的字面量语法,更加地便捷。这种语法也是 JSON 格式的核心,应该经常使用。
对象属性可以链式访问:
obj.details.color; // orange
obj['details']['size']; // 12
数组
JavaScript 中的数组是一种特定类型的对象。它像一般对象一样工作(数值属性仅能通过[]
语法访问),但它有一个魔力属性length
。
创建数组的方式:
var a = new Array();
a[0] = 'dog';
a[1] = 'cat';
a[2] = 'hen';
a.length; // 3
var a = ['dog', 'cat', 'hen'];
a.length; // 3
array.length
并不一定和数组的元素数目有关:
var a = ['dog', 'cat', 'hen'];
a[100] = 'fox';
a.length; // 101
注意,数组的长度比最高的索引大一。
如果你查询一个不存在的数组索引,你会得到undefined
返回值。
如果想在数组后附加值,可以这样实现:
a.push(item);
函数
除了对象,函数是理解 JavaScript 的核心组件。基本的函数定义如下:
function add(x, y) {
var total = x + y;
return total;
}
函数可以在函数体中使用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
这很有用,但看起来有一点啰嗦。我们可以用一种更高效地写法来替代arguments
。
function avg(...args) {
var sum = 0;
for (let value of args) {
sum += value;
}
return sum / args.length;
}
avg(2, 3, 4, 5); // 3.5
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()
。匿名函数有一个使用技巧,那就是隐藏一些局部变量:
var a = 1;
var b = 2;
(function() {
var b = 3;
a += b;
})();
a; // 4
b; // 2
JavaScript 允许函数的递归调用,但是一个匿名函数怎么进行递归呢?我们可以快速调用函数表达式来实现递归:
var charsInBody = (function counter(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 += counter(child);
}
return count;
})(document.body);
自定义对象
在经典的面向对象编程中,对象是数据和操纵数据的方法的集合。而 JavaScript 是基于原型的语言,没有class
关键字,它使用函数作为类。让我们考虑有姓和名的 Person 对象,有两种展示姓名的方法,实现如下:
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;
}
var 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;
}
};
}
var s = makePerson('Simon', 'Willison');
s.fullName(); // "Simon Willison"
s.fullNameReversed(); // "Willison, Simon"
我们看到了this
关键字。在函数中使用 this
会指向当前的对象。它到底指向哪一个对象取决于你调用的方式。如果你在一个对象中使用点操作符调用,那么this
就指向调用的对象。如果没有在调用时使用点操作符,this
就会指向全局对象。
var s = makePerson('Simon', 'Willison');
var fullName = s.fullName;
fullName(); // undefined undefined
当我们单独调用fullName()
,this
就指向了全局对象。因为没有first
,last
这两个全局变量,所以返回undefined
。
我们可以利用this
关键字来优化makefunction
函数:
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
。new
和this
是强关联的。它创建一个新的空对象并调用特定的函数,将this
与新对象绑定。被设计成通过new
调用的函数被称为构造函数,构造函数通常没有返回值和return
关键字。在日常编码中通常首字母大写来区别构造函数。
优化后的函数也有了和单独调用fullName()
时同样的问题。
我们的person
对象变得更好了,但每次创建person
对象时都会在其中新建两个新的函数对象,难道不能让所有的对象共享这两个函数吗?
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
实例共享的对象。它形成了部分原型链:任何时候你尝试访问Person
没有设置的属性时,JavaScript 会检查Person.prototype
是否有该属性,并沿着原型链一直查找下去,直到找到或返回undefined
。也就是说,任何赋给Person.prototype
的变量对所有实例来说都是可见的。
这是一个非常强大的工具。JavaScript 允许你再任何时间修改你程序里的原型,这意味着你可以在运行时为对象添加额外的方法:
var 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 对象原型添加方法。
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
如上所述,原型是链式的。链的根是Object.prototype
,包含toString()
方法。这在调试时非常有用:
var s = new Person('Simon', 'Willison');
s.toString(); // [object Object]
Person.prototype.toString = function() {
return '<Person: ' + this.fullName() + '>';
}
s.toString(); // "<Person: Simon Willison>"
内联函数
JavaScript 中的函数可以在其他函数内部声明。一个重要关于嵌套函数的细节就是它们可以访问父函数的函数作用域:
function parentFunc() {
var a = 1;
function nestedFunc() {
var b = 4; // parentFunc can't use this
return a + b;
}
return nestedFunc(); // 5
}
这在编写可维护的代码上就显得更得心应手。如果一个函数调用一到两个不会再其它地方调用的功能函数,你可以将这些函数嵌套在函数里。这可以减少全局作用域声明的函数数量。
这对全局变量来说也是有利的。当编写复杂的代码时,总是要临时使用全局变量来在多个函数中共享值——这导致代码很难维护。嵌套函数可以共享父函数的作用域,你可以利用这个机制来避免污染你的全局作用域。
闭包
闭包是 JavaScript 提供的最强大的抽象之一,但也是最使人困惑的。
function makeAdder(a) {
return function(b) {
return a + b;
};
}
var x = makeAdder(5);
var y = makeAdder(20);
x(6); // ?
y(7); // ?
makeAdder()
函数在每次调用时都返回了一个新函数adder()
。这很像之前提到的内联函数:一个定义在其它函数体内的函数可以访问外部函数的变量。唯一的区别在于外部函数已经返回了,按理来说它的局部变量也不再存在。但它们确实是存在的,不然新返回的方法将无法工作。也就是说,有两份局部变量的拷贝被分别赋予了两个新创建的函数,一个是5,另一个是20.
x(6); // returns 11
y(7); // returns 27
无论何时 JavaScript 执行一个函数,一个域对象就会被创建并用于保存函数中的局部变量。该函数中创建的嵌套函数将会像接收其他参数一样接收域对象。这和全局对象中全局变量和函数的存在方式很像,但是也有一些不同:首先,一个新的域对象是在函数每次被执行时创建;其次,域对象不能直接被你的 JavaScript 代码访问(例如this
和浏览器中的window
)。
所以,当makeAdder()
被调用时,带有属性a
的域对象被创建了,a
是传递给makeAdder()
的参数。之后返回一个新创建的函数。通常 JavaScript 的垃圾回收机制会清理掉所有域对象,但是返回的函数会维护一条引用到域对象。因此,在没有引用指向域对象之前,域对象不会被垃圾回收机制清理掉。
域对象形成的链称为作用域链,类似于 JavaScript 对象系统的原型链。
闭包是函数和函数调用时创建的域对象的组合。闭包使你可以保持函数状态,正因如此,通常被用于替换对象。