笔记1:非面向对象部分
这是阅读 《JavaScript 高级程序设计》 书籍的学习笔记,整理和归纳,方便自己今后复习和查阅,这里总结的基本上都是一些比较特殊的知识点,和 Java
等其他 高级语言 重复的地方不在归纳范围内。第一部分是基础部分(即非面向对象部分),基本上是围绕变量和函数展开的。
分号
ECMAScript
中的语句以一个分号结尾;如果省略分号,则由解析器确定语句的结尾,如下例所示:
1
2
| var sum = a + b // 即使没有分号也是有效的语句——不推荐
var diff = a - b; // 有效的语句——推荐
|
推荐使用封号
变量
ECMAScript
的变量是松散类型的,所谓松散类型就是可以用来保存任何类型的数据。换句话说,每个变量仅仅是一个用于保存值的占位符而已。定义变量时要使用 var
操作符(注意var是一个关键字),后跟变量名(即一个标识符),如下所示:
此时变量 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(10, 20); //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
类型只有两个字面值 true
和 false
.
实际上,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_INFINITY
和 Number.POSITIVE_INFINITY
也可以得到负和正 Infinity
的值。可以想见,这两个属性中分别保存着 -Infinity
和 Infinity
.
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, 超出即自动转换为用 -Infinity
和 Infinity
。
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("函数声明");
}
|
执行顺序:
字面量对象
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 = [0, 1, 5, 10, 15];
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 = [0, 1, 5, 10, 15];
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 对象的属性。