JavaScript 高级程序设计(第3版)学习笔记

笔记1:非面向对象部分

这是阅读 《JavaScript 高级程序设计》 书籍的学习笔记,整理和归纳,方便自己今后复习和查阅,这里总结的基本上都是一些比较特殊的知识点,和 Java 等其他 高级语言 重复的地方不在归纳范围内。第一部分是基础部分(即非面向对象部分),基本上是围绕变量和函数展开的。

分号

ECMAScript 中的语句以一个分号结尾;如果省略分号,则由解析器确定语句的结尾,如下例所示:

1
2
var sum = a + b                 // 即使没有分号也是有效的语句——不推荐 
var diff = a - b;               // 有效的语句——推荐

推荐使用封号

变量

ECMAScript 的变量是松散类型的,所谓松散类型就是可以用来保存任何类型的数据。换句话说,每个变量仅仅是一个用于保存值的占位符而已。定义变量时要使用 var 操作符(注意var是一个关键字),后跟变量名(即一个标识符),如下所示:

1
var message;

此时变量 message 会保存一个特殊的值 undefined, 定义变量的时候可以赋予初始值:

1
2
var message = "hi";
message = 100;   //有效,不推荐

虽然支持松散类型,但是不推荐给变量设置不同类型的值

局部变量

如果在函数中使用 var 关键字定义变量,作用域只在函数内部,函数执行完后将会被销毁,例如:

1
2
3
4
5
6
function test(){
    var message = "h1"; //局部变量
}

test();
alert(message);  //错误,不能访问到

虽然JS中不存在块级作用域,但是本质上函数是 Function 对象,所以具有封闭性,存在局部访问

变量作用域

JavaScript 没有块级作用域经常会导致理解上的困惑。

1
2
3
4
5
if(true){
    var color = "blue";
}

console.log(color);  // "blue"

if 语句执行完毕后被销毁。但在 JavaScript 中,if 语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中。在使用 for 语句时尤其要牢记这一差异,例如:

1
2
3
4
for (var i=0; i < 10; i++){    
    doSomething(i); 
}
console.log(i);  // 10

一定要注意JS是没有块级作用域的,也就是代码块中的不是局部变量

如果我们在一个函数中省略 var 关键字定义变量,则该变量会提升为全局变量。

1
2
3
4
5
6
7
function add(num1, num2) {     
    sum = num1 + num2;     
    return sum; 
} 

var result = add(1020);    //30 
alert(sum);                  //30

虽然可以通过这种不声明 var 的方式定义全局变量,但是不建议这么做,建议变量都显式声明

数据类型

ECMAScript 中有 5 种 简单数据类型(也称为基本数据类型):Undefined 、 Null 、 Boolean 、 Number 和 String。

可以使用 typeof 操作符来判断类型:

1
2
var message;
console.log(typeof message);   //类型 undefined

基础数据类型可以使用 typeof 来判断,对象(object)类型可以使用 instanceof 关键字判断具体的对象类型

Undefined

Undefined 类型 只有一个值,即特殊的 undefined。在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined.

Null

Null 类型是第二个 只有一个值 的数据类型,这个特殊的值是 null。从逻辑角度来看,null 值 表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回 object 的原因。

1
2
var message = null;
console.log(typeof message);  //类型 object

Boolean

Boolean 类型只有两个字面值 truefalse.

实际上,undefined 值是派生自 null 值的,因此 ECMA-262 规定对它们的相等性测试要返回 true.

1
2
3
4
5
alert(null == undefined);  // true

var message = null;
var statu;
alert(message == statu);   // true

Number

Number 类型有数值范围,访问 Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY 也可以得到负和正 Infinity 的值。可以想见,这两个属性中分别保存着 -InfinityInfinity.

1
2
3
4
5
6
7
var maxNumber = Number.MAX_VALUE;  //最大范围
var minNumber = Number.MIN_VALUE;  //最小范围

console.log(isFinite(maxNumber));  //返回 true
console.log(isFinite(minNumber));  //返回 true
console.log(maxNumber + maxNumber);  //返回 Infinity
console.log(isFinite(maxNumber + maxNumber));  // 返回 false

可以使用 isFinite() 函数判断是否超出了数值的有效范围,如果超出则返回 false, 超出即自动转换为用 -InfinityInfinity

NaN

NaN,即 非数值(Not a Number)是一个特殊的数值,这个数值用于 表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。例如,在其他编程语言中,任何数值除以 0 都会导致错误,从而停止代码执行。但在 ECMAScript 中,任何数值除以 0 会返回 NaN,因此不会影响其他代码的执行。 NaN 本身有两个非同寻常的特点。首先,任何涉及NaN的操作(例如 NaN/10)都会返回 NaN,这个特点在多步计算中有可能导致问题。其次,NaN 与任何值都不相等,包括 NaN 本身。例如,下面的代码会返回 false.

1
2
3
4
5
console.log(NaN == NaN);  //返回 false
console.log(isNaN(NaN));  //返回 true
console.log(isNaN("10")); //false 因为可以转换为Number类型
console.log(isNaN("blog")); //true 不能转换为 Number 类型
console.log(isNaN(true));  //false 可以转换为数值 1

String

String 类型用于表示由零或多个 16 位 Unicode 字符组成的字符序列,即字符串。字符串可以由双引号(")或单引号(')表示,因此下面两种字符串的写法都是有效的:

1
2
var firstName = "Nicholas"; 
var lastName = 'Zakas';

在很多语言中,字符串以对象的形式来表示,因此被认为是引用类型的,ECMAScript 放弃了这一传统

类型转换

类型转换过程首先会调用对象的 valueOf() 方法,如果该方法的返回值不能转换,继续调用 toString() 方法再测试返回值。

有 3 个函数可以把非数值转换为数值类型: Number(), parseInt(), parseFloat().

Number() 方法只判断能否直接转换,不行则返回 NaN,例如:

1
2
3
4
var num1 = Number("Hello world!");      //NaN 
var num2 = Number("");                  //0 
var num3 = Number("000011");            //11 
var num4 = Number(true);                //1

parseInt() 回去寻找有效数值进行转换,例如:

1
2
3
4
5
6
7
var num1 = parseInt("1234blue");        // 1234 
var num2 = parseInt("");                // NaN 
var num3 = parseInt("0xA");             // 10(十六进制数) 
var num4 = parseInt(22.5);              // 22 
var num5 = parseInt("070");             // 56(八进制数) 
var num6 = parseInt("70");              // 70(十进制数)
var num7 = parseInt("0xf");             // 15(十六进制数)

如果要将数值类型转换为字符串只需要使用 toString() 方法即可:

1
2
3
4
5
6
var num = 10; 
alert(num.toString());          // "10" 
alert(num.toString(2));         // "1010" 二进制形式
alert(num.toString(8));         // "12"  八进制形式
alert(num.toString(10));        // "10"  10进制形式
alert(num.toString(16));        // "a"  16进制形式

但是有时候我们不知道变量是否为 null 或者 undefined 这个时候不适合使用 toString(),而应该使用类型转换函数 String(), 这个函数可以将任意类型转换为 String.

1
2
3
4
5
6
7
8
9
var value1 = 10; 
var value2 = true; 
var value3 = null; 
var value4; 

alert(String(value1));     // "10" 
alert(String(value2));     // "true" 
alert(String(value3));     // "null" 
alert(String(value4));     // "undefined"

注意 Number() 和 parseInt() 的区别,toString() 方法和 String() 方法的区别

with 语句

with 语句的作用是将代码的作用域设置到一个特定的对象中。with 语句的语法如下:

1
with(expression) statement;

定义 with 语句的目的主要是为了简化多次编写同一个对象的工作,如下面的例子所示:

1
2
3
var qs = location.search.substring(1); 
var hostName = location.hostname; 
var url = location.href;

上面几行代码都包含 location 对象。如果使用 with 语句,可以把上面的代码改写成如下所示:

1
2
3
4
5
6
7
with(location){     
    var qs = search.substring(1);     
    var hostName = hostname;     
    var url = href; 
}

console.log(qs);  //ie=UTF-8

函数

ECMAScript 函数的参数与大多数其他语言中函数的参数有所不同。ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可以传递一个、三个甚至不传递参数,而解析器永远不会有什么怨言。之所以会这样,原因是 ECMAScript 中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数(如果有参数的话)。如果这个数组中不包含任何元素,无所谓;如果包含多个元素,也没有问题。实际上,在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数。

1
2
3
4
5
6
7
function howManyArgs() {     
    alert(arguments.length); 
} 

howManyArgs("string"45);  //2 
howManyArgs();              //0 
howManyArgs(12);            //1

ECMAScript 函数不能像传统意义上那样实现重载。而在其他语言(如 Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。如前所述,ECMAScirpt 函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的

在 ECMAScirpt 中函数实际上是对象,每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针。

所以上面的函数可以定义如下(这是函数表达式):

1
2
3
var howManyArgs = function(){
    alert(arguments.length);
};

还有一种定义函数的方法就是使用 Function 构造函数:

1
var howManyArgs = new Function("alert(arguments.length)");  //不推荐

注意区分函数表达式和函数声明的区别

我们上面定义了一个函数表达式 howManyArgs 接下来我们来定义一个函数声明:

1
2
3
4
5
howManyArgs();  //先调用,后声明

function howManyArgs(){
    alert(arguments.length);
}

JavaScript 解释器中存在一种 变量声明被提升的机制,也就是说 函数声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面。

我们看一个例子来加深理解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
howManyArgs();  //函数声明被提升,所以执行的是函数声明

var howManyArgs = function(){
    console.log("函数表达式");
};

howManyArgs(); //函数表达式执行完,覆盖了函数声明

function howManyArgs(){
    console.log("函数声明");
}

执行顺序:

1
2
函数声明
函数表达式

字面量对象

Object 类型是所有引用类型的基类,而创建 Object 实例的方式有两种,第一种使用 new 操作符:

1
2
3
var person = new Object(); 
person.name = "Nicholas"; 
person.age = 29;

另一种方式是使用 对象字面量 表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。

1
2
3
4
var person = {     
    name : "Nicholas",     
    age : 29 
};

注意 var person = {};var person = new Object(); 相同。

虽然可以使用前面介绍的任何一种方法来定义对象,但开发人员更青睐对象字面量语法,因为这种语法要求的代码量少,而且能够给人封装数据的感觉。实际上,对象字面量也是向函数传递大量可选参数的首选方式,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function displayInfo(args){
    var output = "";
    if(typeof args.name == "string"){
     	output += "Name: " + args.name + "\n";   
    }
    if(typeof args.age == "number"){
        output += "Age: " + args.age + "\n";
    }
    console.log(output);
}

displayInfo({
    name: "lixiaoqiang",
    age: 28
});

注意在JS中使用字面量对象传递参数是很常用的方式

本文出自水寒的博客:https://dp2px.com, 转载请说明出处。

数组

ECMAScript 5 新增了 Array.isArray() 方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。这个方法的用法如下。

1
2
3
if (Array.isArray(value)){     
    //对数组执行某些操作 
}

ECMAScript 数组也提供了一种让数组的行为类似于其他数据结构的方法(栈方法),例如 pop(), push()

可以使用 sort() 方法对数组排序和比较:

1
2
3
var values = [0151015]; 
values.sort(); 
alert(values);     //0,1,10,15,5

比较:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function compare(value1, value2) {     
    if (value1 < value2) {         
        return 1;     
    } else if (value1 > value2) {         
        return -1;     
    } else {         
        return 0;     
    }
}

var values = [0151015];
values.sort(compare); 
alert(values);    // 15,10,5,1,0

ECMAScript 5 为数组定义了 5 个迭代方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

//every (判断是不是所有元素都满足条件)
var result = numbers.every(function(item, index, array){
   return item > 2; 
});
console.log(result);  //false

//filter (筛选出满足条件的数组)
var filterresult = numbers.filter(function(item, index, array){
    return item > 2;
});
console.log(filterresult);  // [3, 4, 5, 6, 7, 8, 9, 10]

//forEach (可以进行遍历)
var foreatch = numbers.forEach(function(item, index, array){
   console.log(item);
});

//map (每一项运算的结果数组)
var mapresult = numbers.map(function(item,index, array){
    return item > 2;
});
console.log(mapresult);  // [false, false, true, true, true, true, true, true, true, true]

//some (至少有一项成立即可)
var someresult = numbers.some(function(item, index, array){
    return item > 2;
});
console.log(someresult)  //true

ECMAScript 5 还新增了两个缩小数组的方法:reduce()reduceRight()。这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。其中,reduce() 方法从数组的第一项开始,逐个遍历到最后。而 reduceRight() 则从数组的最后一项开始,向前遍历到第一项。

1
2
3
4
5
6
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

var sum = numbers.reduce(function(prev, cur, index, array){
    return prev + cur;
});
console.log(sum); //55

闭包

闭包有两个作用:可以读取函数内部的变量。(解决内部变量外部访问问题),让这些变量的值始终保持在内存中。(方便重复利用这些变量)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function createComparisonFunction(propertyName){

    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    };
}

var obj1 = [5, 9, 10];
var obj2 = [7, 8, 10];
createComparisonFunction(2)(obj1, obj2);  //结果 0

上面例子中的匿名内部函数就是一个闭包,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为其匿名函数的作用域链仍然在引用这个活动对象。

1
2
var comparison = createComparisonFunction(2);
comparison(obj1, obj2);  

直到匿名函数被销毁后 createComparisonFunction() 的活动对象才会被销毁。

1
comparison = null; //解除对匿名函数的引用(以便释放内存)

闭包会携带包含它的函数作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多,建议只在必要时使用

内置对象

ECMA-262 对内置对象的定义是:“由 ECMAScript 实现提供的、不依赖于宿主环境的对象,这些对象在 ECMAScript 程序执行之前就已经存在了。” 意思就是说,开发人员不必显式地实例化内置对象,因为它们已经实例化了。例如:Object、Array、String,还定义了两个单体内置对象:Global 和 Math.

这个 Global 对象是一个终极 “兜底对象”,所有全局作用域中定义的属性和函数都属于该对象,例如前面提到过的 isNaN(), parseInt() 等。ECMAScript 虽然没有指出如何直接访问 Global 对象,但 Web 浏览器都是将这个全局对象作为 window 对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了 window 对象的属性。