我们似乎很容易就会安于现状,怀疑着、踟蹰着不敢向前迈出一步,但等自己真的尝试去做了,霍然回首,原来也不是很难,又成长了一些。 ——龙龙 《随笔》
前言
1995年,netscape公司一位名叫Brendan Eich的工程师创造了JavaScript,当时js名为LiveScript,后来因为sun公司的Java语言的兴起和广泛使用,Netscape出于宣传和推广的考虑,更名为JavaScript。尽管两者之间咩有什么共同点,这便是产生混淆的根源。
与大多数编程语言不同,Js并没有输入或输出的概念。他是一个在宿主环境下运行的脚本语言,任何与外界沟通的机制都是由宿主环境提供的。浏览器是最常见的宿主环境,但在非常多的其他程序中也包含JS解释器,如Adobe Acrobat、Photoshop、SVG图像、Yahoo!的Widget引擎,以及Node.js之类的服务器环境。JS的实际应用远不止这些,还有NoSQL数据库、嵌入式计算机,以及包括GNOME在内的桌面环境等等
概览
JS是一种面向对象的动态语言,它包含类型、运算符、标准内置对象和方法。
- js的语法来源于Java和C,所以这两种语言的许多语法特性同样适用于JavaScropt
- js并不支持类,类的概念在js中通过对象原型得到延续。
- js中的函数也是对象,js允许函数在包含可执行代码的同时,能像其他对象一样被传递
类型
任何编程语言都必不可少的组成部分——类型,js的类型包括(7种)
- Number(数字)
- String (字符串)
- Boolean(布尔值)
- Symbol(符号)(ES6新增)
- Object(对象)(万物皆对象)
- Function(函数)
- Array(数组)
- Date(日期)
- RegExp(正则)
- Null(空)
- Undefined(未定义)
数字
js采用“IEEE 754”标准定义的双精度64位格式表示数字,和其他编程语言不同,js不区分整数和浮点数,所有数字在js中均用浮点数值表示。
在具体实现时,整数值通常被视为32位整型变量。1
0.1+0.2===0.30000000000000004 //true
js支持标准的算术运算符,加减、取模(取余),内置对象Math等1
2Math.sin(3.5);
var d = Math.PI*(r+r)
内置对象parseInt(),将字符串转化成整型,该函数的第二个参数表示字符串所表示的数字的基(进制)1
2parseInt('123',10);// 123
parseInt('11',2); // 3 二进制数字字符串转化成整数值
js还有个类似的内置函数parseFloat(),用以解析浮点数字符串,并且只应用于解析十进制数字
单元运算符 + 也可以吧数字字符串转换成数值1
2+ '42'; // 42
+ '0x10'; // 16 函数把0x解析成16进制
如果给定的字符串不存在数值形式,函数会返回一个特殊的值 NaN1
parseInt('hello',10); //NaN
要小心NaN:如果把NaN作为参数进行数学运算,结果也会是NaN:1
NaN + 5; // NaN
可以使用内置函数isNaN()来判断一个变量是否为NaN:1
isNaN(NaN) //true
js还有两个特殊的值:Infinity(正无穷)和-Infinity(负无穷)1
21/0; //Infinity
-1/0; //-Infinity
可以使用内置函数isFinite()来判断一个变量是否是一个有穷数,如果类型为 Infinity、-Infinity、NaN、则返回false1
2
3
4
5
6
7isFinite(1/0); //false
isFinite(Infinity); //false finite(有限的、有穷的)
isFinite(NaN); //false
isFinite(0); //true Number.isFinite(0); //true
isFinite("0");// true 如果是纯数值类型的检测,则返回false Number.isFinite("0"); //false
isFinite(2e74);//true
- parseInt()和parseFloat()会尝试逐个解析字符串中的字符,直到遇到无法解析的那一个,就返回该字符之前的所有数字;
- 运算符 + ,只要字符串中含有无法解析的数字,就会返回NaN

字符串
js中的字符串是一串 Unicode字符 序列,准确说是一串 UTF-16 编码单元的序列,每一个编码单元由一个16位二进制数表示。每一个Unicode字符由一或两个编码单元表示。
如果想表示一个单独的字符,只需要使用长度为1的字符串1
"hello".length; //5
字符串也有methods(方法)1
2
3"hello".charAt(1); // e
"hello,world".replace("hello","goddbye"); //"goodbey,world"
"hello".toUpperCase(); //HELLO
其他类型
- null 表示空值,必须使用null关键字才能访问。
- undefined 是 未定义 类型的对象,表示一个未初始化的值。一个未被赋值的变量就是undefined类型,undefined实际上是一个不允许修改的值。
- boolean,根据需要js可以根据如下规则将变量转换成布尔类型
- 0、NaN、空字符串(“”)、false、null、undefined 转换为false ( 六种 )
- 其他所有值转换为true
变量
js与其他语言的重要区别是 js中语句块 是没有 作用域的,只有函数有作用域。如果在复合语句中(例如if)使用var声明一个变量,那么他的作用域是整个函数(复合语句 在 函数中)。但是在es6中,let 和 const 关键字允许创建块作用域的变量
运算符
算术运算符+ - * / 和 %(求余)+ 操作符还可以链接字符串(还有 字符串 转换 数字)1
"hello" + "Angela" ;// "helloAngela"
如果你用一个字符串加上一个数字,那么操作数都会首先被转换为字符串1
23 + "2" //"32"
"3" + 4 + 6 //"346"
这里有咩有看出一个实用的技巧——通过空字符串相加,可以将某个变量迅速变成 字符串 类型 
js的比较操作使用<、>、<=、>=,这些运算符对于数字和字符串都通用。
== 和 === , != 和 !== ,区别是有咩有在比较前 进行 类型自适应(类型转换)
控制结构
- if else
- for
- while 和 do-while (循环体 至少执行一次)
例如;1
2
3
4
5
6
7while(a){
a+=1; a为true,才会执行
}
do{
a+=1; //至少执行一次
}while(a>0)
&& 和 || 运算符使用 短路逻辑,是否会执行第二个语句取决于 第一个操作数的结果。在需要访问某个对象的属性时,可以使用这个特性先判断对象是对为空1
var a = Q && Q.length
或运算可以用来设置默认值1
var name = otherName || 'default'
对象
js中的对象表现形式为”名——值”对,类似
- python中的字典
- c/c++中的散列表
- Perl 和 Ruby 中的散列(哈希)
- Java 中的 HashMap
- php中的关联数组
正因为js中的一切(除了核心类型 core object)都是对象,所以js必然与大量的散列表查找操作有着千丝万缕的联系,而散列表擅长的就是告诉查找
有两种简单的方法可以创建一个对象1
var obj = new Object();
和1
var obj = {};
第二种更方便的方法叫做“对象字面量”,这也是json格式的核心语法,一般我们优先选择第二种。
下面的方法创建一个对象原型Person,和原型的对象实例you1
2
3
4
5
6
7
8function Person(name,age){
this.name = name;
thia.age = age;
}
//定义一个对象
var you = new Person('you','20')
创建完成后,对象的属性可以通过如下两种方式进行赋值和访问1
2obj.name = 'angela';
var name = obj.name;
第二种1
2obj['name'] = 'angela';
var name = obj['name'];
这两种方法在语义上是相同的,第二种方法的优点是,属性的名称被看做一个字符串,这意味着她可以在运行时被计算,缺点是
有可能在后期无法被解释器优化,他也可以被用来访问某些以 预留关键字作为名称的属性(注:es5开始, 预留关键字可以作为对象的属性名)
数组
数组是js中一种特殊的对象,他的工作原理与普通对象类似(以数字为属性名,但只能通过[]来访问),他还有一个特殊的属性 length,这个属性的值比最大索引数大1.1
2
3
4
5var a = {'a','b','c'}; //数组字面量比 传统的声明数组更加方便
a.length; // 3
a[100] = 'x';
a.length = 101;
记住:数组的长度是比最大索引数 大 1 的数。
如果试图访问一个不存在的索引,会得到 undefined;1
typeof(a[90]); //undefined
可以通过如下的方式遍历一个数组:1
2
3for(var i = 0; i < a.length; i++){
xxxxx;
}
遍历数组的另一种方法是使用 for…in循环,注意,如果有人向Array.prototype中添加了新的属性,使用for…in循环,这些属性也同样会被遍历。所以不推荐这种方法。1
2
3for(var i in a){
xxx;
}
ES5增加了遍历数组的另一种方法,forEach();1
2
3
4
5['a','b''c'].forEach(function(currentValue,index,array){
// do something with currentValue or array[index]
})
['a','b','c'].forEach(currentValue => console.log(currentValue))
如果想在数组后追加元素,只需要1
a.push(item);
Array类 自带了许多方法。
- a.toString(); //返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔
- a.toLocaleString(); //根据宿主环境的区域设置,返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔
- a.concat(item1,item2,…,itemN); //连接,返回一个数组,包含了原先 a 和 item1,item2,…,itemN中的所有元素
- a.join(sep); //返回一个包含数组中所有元素的字符串,每个元素通过指定的sep分隔
- a.join(sep).splice(sep); //返回原数组
- a.splice(start,delcount,item1,item2,…,itemN);//从start开始,删除delcount个元素。然后插入所有的item
- a.slice(start,end);//返回子数组,以a[start]开头,以a[end]前一个元素结尾。
- a.pop(); //删除并返回 数组中的最后一个元素
- a.shift(); //删除并返回 数组中的第一个元素
- a.unshift(item);//将item插入数组头部,并返回数组新长度(考虑undefined)
- a.push(item1,item2); //将item1,item2追加至数组a末尾
- a.reverse();//数组逆序
- a.sort(cmpfn);//依据cmpfn返回的结果进行排序,如果为指定比较函数,则按字符顺序比较(即使元素是数字)
函数
apply()
apply()方法调用一个函数,其具有一个指定的this值,以及作为一个数组(或 类似数组的对象),提供的参数。
call()方法与apply()方法类似,只有一个区别,就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组
语法1
fun.apply(thisArg,[argsArray])
参数
thisArg
在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正this值,如果这个函数处于非严格模式下,则指定为null或undefined时会自动指向全局对象(浏览器中就是window对象),
同时值为原始值(数字、字符串、布尔值)的this会指向该原始值的自动包装对象
argsArray
一个数组或者类数组对象,其中的数组元素将作为单独的参数传给fun函数。如果该参数的值为null或undefined,则表示不需要传入任何参数

函数
1 | function add(x, y){ |
return 语句返回一个值并结束函数。如果没有使用return函数语句,或者一个没有值的return语句,JavaScript会返回undefined。
函数实际上市访问了函数体中一个名为arguments的内部对象,这个对象就如同一个类似于数组的对象一样,包括了所有被传入的参数。让我们重写一下上面这个函数,使他可以接收任意个数的参数:1
2
3
4
5
6
7
8function 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
求平均数1
2
3
4
5
6
7
8function avg() {
var sum = 0;
for(var i = 0,j = arguments.length.length; i < j; i++){
sum += arguments[i];
}
return sum/arguments.length;
}
avg(2,3,4,5); //3.5
如果处理一个数组的平均值,js允许使用 任意函数对象的 apply() 方法来调用函数,并传递给他一个包含了参数的数组。1
avg.apply(null,[2,3,4,5]); //3.5
js允许以递归的方式调用函数。递归在处理树形结构时非常有用。
自定义对象
在经典的面向对象的语言中,对象是指数据和在这些数据上进行的操作的集合,js是一种基于原型的编程语言,并没与class语句,而是把函数用作类。
我们来定义一个人名对象,名(first) 姓(last);姓(last)名(first)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function makePerson(first,last){
return{
first: first,
last: last,
fullName: function(){
return this.first + ' ' + this.last;
},
fullNameReversed: function(){
return this.lasr + ' ' + this.first;
}
}
}
s = makePerson('Angela','Ma');
s.fullName(); //Angela ma
s.fullNameReversed(); Ma Angela
上面的代码中用到了关键字this,当在函数中使用时,this指代当前的对象,也就是调用了函数的对象。
如果在一个对象上使用 点或者方括号 来访问属性或者方法,这个对象就成了this。
如果并没有使用”点”运算符调用某个对象,那么this将指向全局对象。这是一个经常出错的地方。1
2
3
4s = makePerson('Angela','Ma');
var fullName = s.fullName();
fullName(); //undefined undefined
fullName('Angela','Ma');// undefined undefined
当我们调用 fullName() 时,this实际上是指向全局对象的,并没有名为first或last的全局变量,所以他们两个的返回值都是 unfined.
下面使用关键字this改进已有的函数1
2
3
4
5
6
7
8
9
10
11function 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('Angela','Ma');
我们引入了另外一个关键字:new,他和 this 密切相关。他的作用是创建一个崭新的空对象,然后使用指向那个对象的 this 调用特定的函数。
注意,含有 this 的特定函数不会返回任何值,只会修改 this 对象本身。 new 关键字将生成的 this 对象返回给调用方,而被 new 调用的函数成为构造函数。
这个改进有相同的问题,单独调用 fullName() 还是undefined
虽然我们的perSon已经相当完善了,但是还有可以改进的地方么?当然,每次我们创建一个Person 对象的时候,我们都在其中创建了两个新的函数对象,如果这个代码可以共享不是更好么
1 | function Person(first, last){ |
Person.protoType 是一个可以被 Person 的所有实例共享的对象。他是一个名叫原型链的查询链的一部分:当你试图访问一个 Person 没有定义的属性时,解释器会首先检查这个 Person.prototype 来判断是都存在这样一个属性。
所以,任何分配给 Person.prototype 的东西 对 通过this对象构造的实例 都是可用的。
这个特性功能十分强大,js允许你在程序中的任何时候修改原型中的一些东西,也就是说你可以在运行时给已存在的对象添加额外的方法:
1 | s = new Person("Angela", "Ma"); |

有趣的是,还可以给js的内置函数原型添加东西,让我们给 String 添加一个方法用来返回逆序的字符串:1
2
3
4
5
6
7
8
9
10
11var s = 'Angela';
s.reversed(); // TypeError on line 1: s.reversed is not a function
String.prototype.reversed = function() {
var r = "";
for(var i = this.length; i >= 0; i--){
r += this[i];
}
return r;
}
s.reversed(); //alegnA
定义新方法也可以在字符串字面量上用1
"This can now be reversed".reversed(); // desrever eb won nac sihT
正如我前面提到的,原型组成链的一部分。那条链的根节点是 Object.prototype,它包括 toString() 方法——将对象转换成字符串时调用的方法。这对于调试我们的 Person 对象很有用:
1 | var s = new Person("Simon", "Willison"); |
你是否还记得之前我们说的 avg.apply() 中的第一个参数 null?现在我们可以回头看看这个东西了。apply() 的第一个参数应该是一个被当作 this 来看待的对象。下面是一个 new 方法的简单实现:1
2
3
4
5function trivialNew(constroctor,...args) {
var o = {};
constroctor.apply(o,args);
return o;
}
这并不是 new 的完整实现,因为它没有创建原型(prototype)链。想举例说明 new 的实现有些困难,因为你不会经常用到这个,但是适当了解一下还是很有用的。在这一小段代码里,…args(包括省略号)叫作剩余参数(rest arguments)。如名所示,这个东西包含了剩下的参数。因此调用1
var bill = trivialNew(Person,"Angela","Ma");
可以认为和下面语句是等效的1
var bill = new Person("Angela","Ma");
apply() 有一个姐妹函数,名叫 call,它也可以允许你设置 this,但它带有一个扩展的参数列表而不是一个数组。
1 | function lastNameCaps() { |
内部函数
js允许在一个函数内部定义函数,一个很重要的细节是他们可以访问父函数作用域中的变量:1
2
3
4
5
6
7function betterExampleNeeded() {
var a = 1;
function oneMoewThanA() {
return a + 1;
}
return onMoreThanA;
}
这也是一个减少使用全局变量的好方法。
内部函数可以共享父函数的变量,所以可以使用这个特性把一些函数捆绑在一起,这可以有效的防止”污染”全局命名空间——可以称为“局部全局”。
闭包
1 | function makeAdder(a) { |
makeAdder 函数 :创建了一个新的Adder函数,这个函数自身带有一个参数,他被调用的时候这个参数会被加在外层函数传进来的参数上。
这与内嵌函数十分相似:
- 一个函数被定义在另一个函数内部,内部函数可以访问外部函数的变量
- 唯一不同的是,外部函数被返回了,常识告诉我们局部变量“应该”不存在了,但是仍然存在,否则adder函数将不能工作
到底发生了什么?
每当js执行一个函数时候,都会创建一个作用域对象,用来保存在这个函数中创建的局部变量。他和被传入函数的变量一起被初始化。
这与全局对象和函数的全局对象类似,但仍有一些很重要的区别。
- 第一、每次函数被执行的时候,都会创建一个 新的、特定的 作用域对象;
- 第二、与全局对象不同的是,不能直接从js代码中直接访问作用域对象,也没有可以遍历当前作用域对象里面属性的方法
所以当调用makeAdder时,解释器创建了一个作用域对象,它带有一个属性: a,这个属性 被当做参数传入makeAdder函数。然后makeAdder返回一个新创建的函数。
通常js的垃圾回收器会在这时回收 makeAdder 创建的作用域对象,但是 返回的函数 却保留了一个指向那个 作用域对象的引用。 结果是这个作用域对象不会被垃圾回收器回收,
直到指向 makeAdder 返回的那个 函数对象的引用 计数为零。
作用域对象组成了一个名为 作用域链 的链。它类似于原型链一样,被js的对象系统使用。
一个闭包 就是一个函数 和 被创建的函数中的作用域对象 的组合。
内存泄漏
使用闭包的一个坏处是,在IE浏览器中它容易导致 内存泄漏。js是一种具有垃圾回收机制的语言——对象再被创建的时候 分配内存,然后当指向这个对象的引用计数为零时,浏览器会回收内存。宿主环境提供的对象都是按照这种方法被处理的。
浏览器需要处理大量的对象来描绘一个正在被展现的html页面——DOM对象。浏览器负责管理他们的内存分配和回收。
IE浏览器有自己的一套垃圾回收机制,这套机制与js提供的垃圾回收机制进行交互的时候,可能会发生内存泄漏。
在IE中,每当一个js对象 和 一个本地对象 之间形成循环引用时,就会发生内存泄漏。1
2
3
4
5
6// bad
function leakMemory() {
var el = document.getElementById('el');
var o = {'el':el};
el.o = o;
}
这段代码的循环引用会导致内存泄漏:IE 不会释放被 el 和 o 使用的内存,直到浏览器被彻底关闭并重启后。
一般也很少发生如此明显的内存泄漏现象——通常泄漏的数据结构有多层的引用,往往掩盖了循环引用的情况。
闭包很容易发生 无意识的内存泄漏。1
2
3
4
5
6function addHandler() {
var el = document.getElementById('el');
el.onclick = function() {
el.style.backgroundColor = 'red';
}
}
这段代码创建了一个元素,当他被点击的时候变红,但同时也发生了内存泄漏。
为什么?
因为对 el 的引用不小心 放在一个匿名内部函数中。这就在js对象(内部函数) 和 本地对象(el)之间创建了一个循环引用。
这个问题有很多种解决办法,最简单的一种就是不要使用 el 变量1
2
3
4
5function addHandler() {
document.getElementById('el').onclick = function() {
this.style.backgroundColor = 'red';
}
}
另外一种避免闭包的好方法就是在 windows.onunload 事件发生期间破坏 循环引用。许多事件库都能完成这项工作。注意这样做将使 Firefox 中 的becache 无法工作。
所以除非有其他必要的原因,最好不要在Firefox中注册一个 onunload 的监听器。