# JavaScript网道教程-学习笔记(重点记录)
## 导论
各种宿主机环境提供额外的API(即只能在该环境使用的接口),以便JavaScript调用,以浏览器为例,它提供的额外API可以分成三大类:
- 浏览器控制类:操作浏览器;
- DOM类:操作网页文档的各种元素;
- Web类:实现互联网的各种功能;
JavaScript的核心语法精简,但是其复杂性体现在其他两个方面:
- 首先,它涉及大量的外部 API。JavaScript 要发挥作用,必须与其他组件配合,这些外部组件五花八门,数量极其庞大,几乎涉及网络应用的各个方面,掌握它们绝非易事;
- 其次,JavaScript 语言有一些设计缺陷。某些地方相当不合理,另一些地方则会出现怪异的运行结果。学习 JavaScript,很大一部分时间是用来搞清楚哪些地方有陷阱;
JavaScript 的性能优势体现在以下方面:
- **灵活的语法,表达力强。**
- **支持编译运行。**
- **事件驱动和非阻塞式设计。**
进入JavaScript实验环境的快捷键(Chrome):
- Mac:Option + Command + J
- Win/Linux:Ctrl + Shift + J
进入开发者工具快捷键(Chrome):
- Mac:Option + Command + I
- Win/Linux:Ctrl + Shift + I
---
## 历史
---
## 基本语法
### 变量提升
> JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
### 区块
> JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。
>
> 对于`var`命令来说,JavaScript 的区块不构成单独的作用域(scope)。
### 条件语句
> JavaScript 提供`if`结构和`switch`结构,完成条件判断,即只有满足预设的条件,才会执行相应的语句。
#### if
注意,`if`后面的表达式之中,不要混淆赋值表达式(`=`)、严格相等运算符(`===`)和相等运算符(`==`)。尤其是赋值表达式不具有比较作用。
为了避免将`==`写成`=`的情况,有些开发者习惯将常量写在运算符的左边,这样的话,一旦不小心将相等运算符写成赋值运算符,就会报错,因为常量不能被赋值,如下:
```javascript
if (x = 2) { // 不报错
if (2 = x) { // 报错
```
多个判断分支时,使用`if...else if...else`
#### switch
`switch`语句部分和`case`语句部分,都可以使用表达式。
> 需要注意的是,`switch`语句后面的表达式,与`case`语句后面的表示式比较运行结果时,采用的是严格相等运算符(`===`),而不是相等运算符(`==`),这意味着比较时不会发生类型转换。
#### 三目运算符
```shell
(条件) ? 表达式1 : 表达式2
```
### 循环
- while
- for
- do...while
### 标签(label)
> JavaScript 语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下:
>
> label:
>
> 语句
给的样例:
```javascript
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) break top; //当break执行时,直接跳出双重循环
console.log('i=' + i + ', j=' + j);
}
}
```
break、continue都可以结合label来使用,可以跳出多重循环,也可以用来跳出区块...
---
## 数据类型
### 确定value类型的三种方式
- typeof运算符
- instanceof运算符
- Object.prototype.toString方法
```javascript
typeof null; //"object"
```
> `null`的类型是`object`,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑`null`,只把它当作`object`的一种特殊值。后来`null`独立出来,作为一种单独的数据类型,为了兼容以前的代码,`typeof null`返回`object`就没法改变了。
### null 与 undefined
> `null`与`undefined`都可以表示“没有”,含义非常相似。将一个变量赋值为`undefined`或`null`,老实说,语法效果几乎没区别。
```javascript
var a = undefined;
// 或者
var a = null;
```
上面代码中,变量`a`分别被赋值为`undefined`和`null`,这两种写法的效果几乎等价。
在`if`语句中,它们都会被自动转为`false`,相等运算符(`==`)甚至直接报告两者相等。
用法与含义:
> 对于`null`和`undefined`,大致可以像下面这样理解。
>
> `null`表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入`null`,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入`null`,表示未发生错误。
>
> `undefined`表示“未定义”,下面是返回`undefined`的典型场景。
>
> ```javascript
> // 变量声明了,但没有赋值
> var i;
> i // undefined
>
> // 调用函数时,应该提供的参数没有提供,该参数等于 undefined
> function f(x) {
> return x;
> }
> f() // undefined
>
> // 对象没有赋值的属性
> var o = new Object();
> o.p // undefined
>
> // 函数没有返回值时,默认返回 undefined
> function f() {}
> f() // undefined
> ```
### 布尔值
JavaScript中会返回布尔值的运算符:
- 前置逻辑运算符: `!` (Not)
- 相等运算符:`===`,`!==`,`==`,`!=`
- 比较运算符:`>`,`>=`,`<`,`<=`
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为`false`,其他值都视为`true`。
- `undefined`
- `null`
- `false`
- `0`
- `NaN`
- `""`或`''`(空字符串)
注意,空数组(`[]`)和空对象(`{}`)对应的布尔值,都是`true`。
### 数值
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,`1`与`1.0`是相同的,是同一个数。
这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算,参见《运算符》一章的“位运算”部分。
#### 数值精度
根据国际标准 IEEE 754,JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的。
- 第1位:符号位,`0`表示正数,`1`表示负数
- 第2位到第12位(共11位):指数部分
- 第13位到第64位(共52位):小数部分(即有效数字)
精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-253到253,都可以精确表示。
#### 数值范围
也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为21024到2-1023(开区间),超出这个范围的数无法表示。
#### 数值的表示
以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示:
1. **小数点前的数字多于21位**;
2. **小数点后的零多于5个**;
#### 数值的进制
使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。
- 十进制:没有前导0的数值。
- 八进制:有前缀`0o`或`0O`的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
- 十六进制:有前缀`0x`或`0X`的数值。
- 二进制:有前缀`0b`或`0B`的数值。
#### 特殊数值
- 正零、负零
- NaN
`NaN`是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合;
`NaN`不等于任何值,包括它本身;
数组的`indexOf`方法内部使用的是严格相等运算符,所以该方法对`NaN`不成立;
`NaN`在布尔运算时被当作`false`;
`NaN`与任何数(包括它自己)的运算,得到的都是`NaN`
- Infinity
---
### 字符串
---
### 对象
> 对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
eg:
```javascript
var obj = {
p: function (x) {
return 2 * x;
}
};
obj.p(1) // 2
```
> 如果属性的值还是一个对象,就形成了链式引用。
eg:
```javascript
var o1 = {};
var o2 = { bar: 'hello' };
o1.foo = o2;
o1.foo.bar // "hello"
```
#### 对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量:
```javascript
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
```
此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量:
```javascript
var o1 = {};
var o2 = o1;
o1 = 1;
o2 // {}
```
#### 这是表达式还是语句?
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
```javascript
{ foo: 123 }
```
JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含`foo`属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签`foo`,指向表达式`123`。
为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。
如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。(但是这样子有意义吗?匿名对象?)
这种差异在使用eval语句时表现最明显,可以用eval语句来声明赋值一个变量是对象还是别的类型:
```javascript
var a = eval('{foo: 123}') // 123
var b = eval('({foo: 123})') // {foo: 123}
a // 123
b // {foo: 123}
```
#### 属性读取需要注意的地方
```javascript
var foo = 'bar';
var obj = {
foo: 1,
bar: 2
};
obj.foo // 1
obj[foo] // 2
```
#### 属性的删除
另外,需要注意的是,`delete`命令只能删除对象本身的属性,无法删除继承的属性;
#### 属性是否存在:使用`IN`运算符
`in`运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回`true`,否则返回`false`。它的左边是一个字符串,表示属性名,右边是一个对象。eg:
```javascript
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true
```
`in`运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象`obj`本身并没有`toString`属性,但是`in`运算符会返回`true`,因为这个属性是继承的。
这时,可以使用对象的`hasOwnProperty`方法判断一下,是否为对象自身的属性。eg:
```javascript
var obj = {};
if ('toString' in obj) {
console.log(obj.hasOwnProperty('toString')) // false
}
```
#### 属性的遍历:使用`for...in`循环
`for...in`循环用来遍历一个对象的全部属性:
```javascript
var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
console.log('键名:', i);
console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3
```
`for...in`循环有两个使用注意点。
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性;
- 它不仅遍历对象自身的属性,还遍历继承的属性;
举例来说,对象都继承了`toString`属性,但是`for...in`循环不会遍历到这个属性:
```javascript
var obj = {};
// toString 属性是存在的
obj.toString // toString() { [native code] }
for (var p in obj) {
console.log(p);
} // 没有任何输出
```
(方法是不可遍历的?)
#### with语句
`with`语句的格式如下:
```javascript
with (对象) {
语句;
}
```
它的作用是操作同一个对象的多个属性时,提供一些书写的方便:
```javascript
// 例一
var obj = {
p1: 1,
p2: 2,
};
with (obj) {
p1 = 4;
p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;
// 例二
with (document.links[0]){
console.log(href);
console.log(title);
console.log(style);
}
// 等同于
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);
```
注意,如果`with`区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量:
```javascript
var obj = {};
with (obj) {
p1 = 4;
p2 = 5;
}
obj.p1 // undefined
p1 // 4
```
弊病在于:`with`区块没有改变作用域,它的内部依然是当前作用域。这造成了`with`语句的一个很大的弊病,就是绑定对象不明确。eg:
```javascript
with (obj) {
console.log(x);
}
```
单纯从上面的代码块,根本无法判断`x`到底是全局变量,还是对象`obj`的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用`with`语句,可以考虑用一个临时变量代替`with`:
```javascript
with(obj1.obj2.obj3) {
console.log(p1 + p2);
}
// 可以写成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);
```
---
## 函数
### 函数的声明
1. function 命令
2. 函数表达式
`var f = function f() {};`
3. Function构造函数
### 函数的重复声明
> 如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
eg:
```javascript
function f() {
console.log(1);
}
f() // 2
function f() {
console.log(2);
}
f() // 2
```
上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升,前一次声明在任何时候都是无效的,这一点要特别注意。
### 函数是第一等公民
> 函数只是一个可以执行的值,此外并无特殊之处。
>
> 由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
eg:
```javascript
function add(x, y) {
return x + y;
}
// 将函数赋值给一个变量
var operator = add;
// 将函数作为参数和返回值
function a(op){
return op;
}
a(add)(1, 1) // 第二个圆括号是第一个圆括号中传入的函数所需的参数
// 2
```
### 函数名提升
由于函数是第一等公民,其地位与其他数据类型相等,而变量存在"变量"提升,巧了,函数名也存在"提升";
> JavaScript 引擎将函数名视同变量名,所以采用`function`命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错:
>
> ```javascript
> f();
>
> function f() {}
> ```
但是,如果采用赋值语句定义函数,JavaScript 就会报错:
```javascript
f();
var f = function (){};
// TypeError: undefined is not a function
```
上面的代码等同于:
```javascript
var f;
f();
f = function () {};
```
上面代码第二行,调用`f`的时候,`f`只是被声明了,还没有被赋值,等于`undefined`,所以会报错。因此,如果同时采用`function`命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义:
```javascript
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
```
### 函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
```javascript
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
```
上面代码中,函数`x`是在函数`f`的外部声明的,所以它的作用域绑定外层,内部变量`a`不会到函数`f`体内取值,所以输出`1`,而不是`2`。
**总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。**
**同样的,函数体内部声明的函数,作用域绑定函数体内部。**
```javascript
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
```
上面代码中,函数`foo`内部声明了一个函数`bar`,`bar`的作用域绑定`foo`。当我们在`foo`外部取出`bar`执行时,变量`x`指向的是`foo`内部的`x`,而不是`foo`外部的`x`。正是这种机制,构成了“闭包”现象。
### 函数参数的传递方式
- 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
- 但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
**注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值:**
```javascript
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
```
上面代码中,在函数`f`内部,参数对象`obj`被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(`o`)的值实际是参数`obj`的地址,重新对`o`赋值导致`o`指向另一个地址,保存在原地址上的值当然不受影响。
### 同名参数
- 如果有同名的参数,则取最后出现的那个值。
- 即使后面的`a`没有值或被省略,也是以其为准。
```javascript
function f(a, a) {
console.log(a);
}
f(1) // undefined
```
调用函数`f`的时候,没有提供第二个参数,`a`的取值就变成了`undefined`。这时,如果要获得第一个`a`的值,可以使用`arguments`对象:
```javascript
function f(a, a) {
console.log(arguments[0]);
}
f(1) // 1
```
### arguments对象
> 由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是`arguments`对象的由来。
>
> `arguments`对象包含了函数运行时的所有参数,`arguments[0]`就是第一个参数,`arguments[1]`就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
正常模式下,`arguments`对象可以在运行时修改:
```javascript
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5
```
严格模式下,`arguments`对象与函数参数不具有联动关系。也就是说,修改`arguments`对象不会影响到实际的函数参数:
```javascript
var f = function(a, b) {
'use strict'; // 开启严格模式
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 2
```
#### 与数组的关系
*需要注意的是,虽然`arguments`很像数组,但它是一个对象。数组专有的方法(比如`slice`和`forEach`),不能在`arguments`对象上直接使用。*
如果要让`arguments`对象使用数组方法,真正的解决方法是将`arguments`转为真正的数组。下面是两种常用的转换方法:`slice`方法和逐一填入新数组:
```javascript
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
```
#### callee属性
`arguments`对象带有一个`callee`属性,返回它所对应的原函数。
```javascript
var f = function () {
console.log(arguments.callee === f);
}
f() // true
```
> 可以通过`arguments.callee`,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。
### **闭包(closure)**
> 理解闭包,首先必须理解变量作用域。
> 如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
#### 链式作用域
JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
eg:
```javascript
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
```
上面代码中,函数`f2`就在函数`f1`内部,这时`f1`内部的所有局部变量,对`f2`都是可见的。但是反过来就不行,`f2`内部的局部变量,对`f1`就是不可见的。
#### 闭包的特点与作用
> 闭包最大的特点,就是它可以“记住”诞生的环境,比如`f2`记住了它诞生的环境`f1`,所以从`f2`可以得到`f1`的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果:
```javascript
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
```
闭包的另一个用处,是封装对象的私有属性和私有方法,eg:
```javascript
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
```
上面代码中,函数`Person`的内部变量`_age`,通过闭包`getAge`和`setAge`,变成了返回对象`p1`的私有变量。
**注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。**
### 立即调用的函数表达式(IIFE)
> 有时,我们需要在定义函数之后,立即调用该函数。
**通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。**
eg:
```javascript
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 写法二
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
```
上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。
### eval命令
`eval`命令接受一个字符串作为参数,并将这个字符串当作语句执行:
```javascript
eval('var a = 1;');
a // 1
```
如果参数字符串无法当作语句运行,那么就会报错:
```javascript
eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
```
放在`eval`中的字符串,应该有独自存在的意义,不能用来与`eval`以外的命令配合使用。举例来说,下面的代码将会报错:
```javascript
eval('return;'); // Uncaught SyntaxError: Illegal return statement
```
上面代码会报错,因为`return`不能单独使用,必须在函数中使用。
如果`eval`的参数不是字符串,那么会原样返回:
```javascript
eval(123) // 123
```
`eval`没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题:
```javascript
var a = 1;
eval('a = 2');
a // 2
```
为了防止这种风险,JavaScript 规定,如果使用严格模式,`eval`内部声明的变量,不会影响到外部作用域:
```javascript
(function f() {
'use strict';
eval('var foo = 123');
console.log(foo); // ReferenceError: foo is not defined
})()
```
> 总之,`eval`的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。通常情况下,`eval`最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的`JSON.parse`方法。
`eval`的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨`eval()`这一种形式是直接调用。
---
## 数组
### 数组的本质
网友评论