美文网首页
JavaScript网道教程-学习笔记

JavaScript网道教程-学习笔记

作者: Xxthon | 来源:发表于2019-08-01 17:19 被阅读0次

# 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()`这一种形式是直接调用。

---

## 数组

### 数组的本质

相关文章

网友评论

      本文标题:JavaScript网道教程-学习笔记

      本文链接:https://www.haomeiwen.com/subject/yzecdctx.html