本周任务:
- 复习JS所学知识
- 学习C语言操作文件的方法
- 总结C语言知识
JS复习
完整的JavaScript实现包括三个部分:
- ECMAScript:核心
- DOM:文档对象模型
- BOM:浏览器对象模型
1. 基础语法
1.1 语法
- 区分大小写:ECMAScript中一切都区分大小写。
- 标识符:变量、属性、函数或函数参数的名称。
标识符可以是按照下列格式规则组合起来的一或多个字符: 第一个字符必须是一个字母、下划线( _ ) 或一个 美元符号( $ )。 其它字符可以是字母、下划线、美元符号或数字。 按照惯例,ECMAScript 标识符采用驼峰命名法。 标识符不能是关键字和保留字符。
- 注释:ECMAScript采用C语言风格的注释,包括单行注释和块注释。
单行注释:// 注释内容 块注释:
也可以写成:
- 严格模式:ECMAScript5新增了严格模式,ECMAScript3中不规范写法在在严格模式下会被处理,对于不安全的活动将抛出错误。
对整个脚本启用严格模式:"use strict"; 单独指定某个函数在严格模式下执行:
function doSomething (){
"use strict";
}
语句语法: ????每一条语句都要以; 结束 ????一条语句必须在同一行内,不能换行 ????每一条语句都要独占一行(编码规范)
JS语句块 :一组相关的代码的集合 ????用{ } 括起来的一些语句组成语句块,用在流程控制、函数或对象中。 ????JavaScript代码的执行次序与书写次序相同
1.2 关键字与保留字
关键字与保留字都不能用作标识符。
- 关键字:有特殊用途或者执行特定的操作
 - 保留字:在语言中没有特定用途,但它们是保留给将来作为关键字使用的

1.3. 变量
变量的作用是给某一个值或对象标注名称。
var a;
a = 123;
var a = 123;
1.4 数据类型
JavaScript中一共有5种基本数据类型:
-
数值类型(number ) 一切数字都是数值类型(包括二进制,十进制,十六进制等) NAN(not number),一个非数字 -
字符串类型(string ) 被一对引用包裹的所有内容(单引号或者双引号) -
布尔类型(boolean ) 只有两个值(true 或者 false) -
null类型(null ) 只有一个取值,就是null,表示空的意思 -
undefined类型(undefined ) 只有一个取值,就是undefined,表示没有值的意思
1.5 判断数据类型的关键字
isNaN 关键字 isNAN可以判断一个变量是不是数
var n1 = 100;
console.log(isNaN(n1));
var s1 = 'Jack'
console.log(isNaN(s1));
typeof 关键字 JavaScript有分数据类型,那么我们有时候需要知道我们存储的数据是什么类型的数据,使用 typeof 关键字来进行判断
语法: typeof 变量
var age = 18;
var name=" ";
var isFun = true;
var a;
console.log(typeof age);
console.log(typeof name);
console.log(typeof a);
console.log(typeof isFun);
1.6 数据类型的转换
- 其他类型转换为Boolean类型
转换方式 Boolean() value?true:false !!value
console.log(Boolean(undefined));
console.log(Boolean(null));
console.log(Boolean(0));
console.log(Boolean(NaN));
console.log(Boolean(1));
console.log(Boolean(""));
console.log(Boolean("abc"));
console.log(Boolean({}));
if (new Boolean(false)) {
console.log("执行");
}
- 其他类型转换为Number类型
转换方式 Number() +value parseFloat parseInt
console.log(Number(undefined));
console.log(Number(null));
console.log(Number(true));
console.log(Number(false));
console.log(Number(""));
console.log(Number("abc"));
console.log(Number("123.345xx"));
console.log(Number("32343,345xx"));
console.log(Number({ x: 1, y: 2 }));
console.log(parseFloat("123.345xx"));
console.log(parseFloat("32343,345xx"));
console.log(parseInt("123.345xx"));
console.log(parseInt("32343,345xx"));
- 其他类型转换为String类型
转换方式 String() ‘’+value value.toString();
console.log(String(undefined));
console.log(String(null));
console.log(String(true));
console.log(String(false));
console.log(String(0));
console.log(String(234));
console.log(String({ x: 1, y: 2 }));
- 隐式类型转换
使用关系运算符时的转换(==、>、<、引用类型和基本类型比较时) 使用算数运算符时的转换(‘img’+ 3 + ‘.jpg’; “25”-0;) 使用逻辑运算符时的转换( !!0; ) 执行流程语句时的转换(if(obj){…})
var a = 1;
var b = 2;
console.log(a > b, typeof(a > b));
var c = 3 + '2';
var d = '3' - 2
var e = !!a;
console.log(typeof e);
- 显式类型转换(使代码更清晰)
Boolean()、Number()、String()、Object() 数转为字符串(toString()、toFixed()、toPrecision()、toExponential()) 字符串转为数字(parseInt()、parseFloat()) 对象转换为原始值(toString()、valueOf())
1.7 运算符
运算符也叫操作符,通过运算符可以对一个或多个值进行运算并获取运算结果。
- 算术运算符
+ 加法运算 只有符号两边都是数字的时候,才会进行加法运算 只要符号任意一边是字符串,就会进行字符串的拼接- 减法 会执行减法运算 会自动把两边都转换成数字进行运算* 乘法 会执行乘法运算 会自动把两边都转换成数字进行运算/ 除法 会执行除法运算 会自动的两边都转换成数字进行运算% 求余 会执行取余运算 会自动把两边都转换成数字进行运算= 赋值运算 就是把 = 右边的值 赋值给 = 左边的变量名 var num = 100;+=
var age = 18;
a += 10;
console.log(a)
var a = 10;
a -= 10;
console.log(a);
var a = 10;
a *= 10;
console.log(a);
var a = 10;
a /= 10;
console.log(a);
var a = 10;
a %= 10;
console.log(a);
- 关系运算符
-
== 比较符号两边的值是否相等,不管数据类型 1 == '1' ==>>true 两个值是一样的,所以得到true -
=== 全等 比较符号两边的值 和数据类型是否相等 1 === '1'==>false 两个值虽然一样,但是因为因为值得数据类型不一样,所以为false -
!= 比较符号 两边的值是否 不等 1 != '1'==>false 因为两边的值是相等的,所以在比较的他们不等的时候得到false -
!== 不全等 比较符号两边的值和类型是否不等 1 != '1' true 因为量的数据类型不一样,所以得到true -
>= 比较左边的值是否 大于或等于 右边的值 1 >= 1 true 1 >= 0 true 1 >= 2 false -
<= 比较左边的值是否 小于或等于 右边的值 1 <= 2 true 1 <= 1 true 1 <= 0 false -
> 比较左边的值是否 大于 右边的值 1 > 0 true 1 > 1 false 1 > 2 false -
< 比较左边的值是否 小于 右边的值 1 < 2 true 1 < 1 false 1 < 0 false
- 逻辑运算符
&& 进行 且 的运算 符号左边必须为 true 并且右边也是 true,才会返回 true 只要有一边不是 true,那么就会返回 false true && true true true && false false false && true false false && false false|| 进行 或 的运算 符号的左边为 true 或者右边为 true,都会返回 true 只有两边都是 false 的时候才会返回 false true || true true true || false true false || true true false || false false! 进行 取反 运算 本身是 true 的,会变成 false 本身是 false 的,会变成 true !true false !false true
- 自增自减运算(一元运算符)
++ 进行自增运算。分成两种,前置++ 和 后置++
前置++,会先把值自动 +1,再返回
var a = 10;
console.log(++a);
var num = 10;
num++
console.log(num)
后置++,会先把值返回,在自动+1
var a = 10;
console.log(a++);
-- 进行自减运算 分成两种,前置-- 和 后置– 和 ++ 运算符道理一样
- 条件运算符
JavaScript 还包含了基于某些条件对变量进行赋值的条件运算符。
语法:variablename=(condition)?value1:value2; 执行流程:如果condition为true,则执行语句1,并返回执行结果,如果为false,则执行语句2,并返回执行结果。
-
逗号运算符 使用逗号可以在一条语句中执行多次操作。 比如:var num1=1, num2=2, num3=3; 使用逗号运算符分隔的语句会从左到右顺 序依次执行。 -
运算符优先级 运算符优先级由上到下依次减小,对于同级运算符,采用从左向右依次执行的方法。 
1.8代码块
-
语句 前边我所说表达式和运算符等内容可以理解成是我们一 门语言中的单词,短语。而语句(statement)就是我们这个语言中一句一句完 整的话了。语句是一个程序的基本单位,JavaScript的程序就是由一条一条语句构成的,每一条语句使用; 结尾。 JavaScript中的语句默认是由上至下顺序执行的,但是我们也可以通过一些流程控制语句来控制语句的执行顺序。 -
代码块 代码块是在大括号 {} 中所写的语句,以此将多条语句的集合视为一条语句来使用。 例如:
{
var a = 123;
a++;
alert(a);
}
一般使用代码块将需要一起执行的语句进行分组,需要注意的是,代码块结尾不需要加分号。
1.9 流程控制语句
- 条件语句
条件语句是通过判断指定表达式的值来决定执行还是跳过某些语句,最基本的条件语句: if…else switch…case
if…else if…else语句是一种最基本的控制语句,它让JavaScript可以有条件的执行语句。 第一种形式:
if(expression) ??????statement
var age = 16;
if (age < 18) {
console.log("未成年");
}
第二种形式:
if(expression) ??????statement else ??????statement
var age = 16;
if (age < 18) {
console.log("未成年");
} else {
console.log("已成年");
}
第三种形式:
if(expression1) ??????statement else if(expression2) ??????statement else ??????statement
var age = 18;
if (age < 18) {
console.log("小于18岁了");
} else if (age == 18) {
console.log("已经18岁了");
} else {
console.log("大于18岁了")
}
switch…case switch…case是另一种流程控制语句。 switch语句更适用于多条分支使用同一条语句的情况。 语法格式: switch (语句) { ??????case 表达式1: ?????? ??????语句... ??????case 表达式2: ?????? ??????语句... ??????default: ?????? ??????语句... } 一旦符合case的条件程序会一直运行到结束,所以一般会在case中添加break作为语句的结束。
var today = 1;
switch (today) {
case 1:
console.log("星期一");
break;
case 2:
console.log("星期二");
break;
case 3:
console.log("星期三");
break;
case 4:
console.log("星期四");
break;
case 5:
console.log("星期五");
break;
case 6:
console.log("星期六");
break;
case 7:
console.log("星期日");
break;
default:
console.log("输入错误");
}
- 循环语句
循环语句和条件语句一样,也是基本的控制语句,只要满足一定的条件将会一直执行,最基本的循环语句: while do…while for
while while语句是一个最基本的循环语句,while语句也被称为while循环。 语法格式: while(条件表达式){ ??????语句... }
var i = 1;
while (i <= 10) {
console.log(i);
i++;
}
do…while do…while和while非常类似,只不过它会在循环的尾部而不是顶部检查表达式的值,因此,do…while循环会至少执行一次。相比于while,do…while的使用情况并不 是很多。 语法格式: do{ ??????语句... }while(条件表达式);
var i = 1;
do {
console.log(i);
i++;
} while (i <= 10);
for for语句也是循环控制语句,我们也称它为for循环。大部分循环都会有一个计数器用以控制循环执行的次数, 计数器的三个关键操作是初始化、检测和更新。for语句 就将这三步操作明确为了语法的一部分。 语法格式: for(初始化表达式 ; 条件表达式 ; 更新表达式){ ??????语句... }
for (var i = 1; i <= 10; i++) {
console.log(i);
}
- 跳转控制
break:结束最近的一次循环,可以在循环和switch语句中使用。 continue:结束本次循环,执行下一次循环,只能在循环中使用。 如果想要跳出多层循环或者跳到指定位置,可以为循环语句创建一个label,来标识当前的循环,如:
outer: for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (j == 5) {
break outer;
}
console.log(j);
}
}
2.变量作用域
2.1 原始值与引用值
????ECMAScript变量可以包含两种不同类型的数据:原始值和引用值。在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。
原始值:原始值是存储在栈中的简单数据段,它们的值直接存储在变量访问的位置。原始值表示表示单一的数据,保存原始值的变量是按值访问,操作存储在变量内存中的实际值。ECMAScript 中设计了6种原始值:Undefined、Null、Boolean、Number、String和Symbol。
引用值:引用值是存储在堆中的对象,存储在变量处的值是一个指针,指向存储对象的内存处引用值表示表示有多个值(原始值或其他引用值)构成的对象。保存引用值的变量是按引用访问的。实际操作对象时,访问的是保存对象的内存地址,即该对象的引用(ECMAScript 不允许直接访问对象的内存空间)。
- 动态属性
????在定义方式上,原始值和引用值很类似,都是先创建一个变量,再给它赋一个值。二者真正的区别在于变量保存了值之后,对这个值如何操作。 ????对于引用值而言,赋给变量后可以随时添加、修改和删除该变量的属性。而对于原始值原始值不能有属性,尽管给原始值添加属性不会报错。故 只有引用值可以动态添加后面可以使用的属性。
注意:原始值的初始化可以只使用原始字面量形式。若初始化时使用 new 关键字创建了对象,则 JavaScript 会为其创建一个 Object 类型的实例,但其行为仍旧与原始值类似。
-
复制值 原始值与引用值在通过变量赋值时也不同。 ????原始值变量存储在栈中,在通过变量把一个原始值赋给另一个原始值时,原始值会被复制到新变量的位置。此后新变量与原变量各自独立,互不干扰。改变其中一个变量的值不会使另一个变量值发生改变。 ????引用值在被从一个变量赋给另一个变量时,是在新变量中存放指向原变量的指针,因此新变量与原变量都指向同一个对象,就是要被赋给新变量的值。此后,这个对象的值会使两个变量的值都发生改变。 -
传递参数 ????ECMAScript中所有参数都是按值传递的,这意味着函数外的值会像变量值的复制一样被复制到函数内部的参数中。 根据变量值的复制方法可以知道: ????????在按值传递参数时,值会被复制到一个局部变量,这完全可以实现; ????????而按引用传递参数时,值在内存中的位置会被保留到一个局部变量,这意味着对该局部变量的修改会反映到函数外部(即使该局部变量所在函数的外部的原参数变量的值也发生改变),这在 ECMAScript 中是不可能实现的。 ????????故参数传递只能按值传递。 注意:ECMAScript 中函数的参数就是局部变量。 -
确定类型
typeof操作符:用于判断一个原始值变量的类型,是否为字符串、数值、布尔值或 undefined。 instanceof操作符:用于判断引用值变量是什么类型的对象,语法为 result = variable instanceof constructor。返回值为 true 或者 false.
根据引用值的定义(是由多个值构成的对象),所有的引用值都是 Object 类型的实例,因此通过 instanceof 操作符检测任何引用值是否是 Object 构造函数都会返回 true。 同样,原始值不是对象,故用 instanceof 检测任何原始值都会返回 false。
注意:typeof操作符在用于检测函数时也会返回 “function”。typeof 对正则表达式会返回 “function” 或者 “object”,取决于浏览器。
2.2 执行上下文与作用域
执行上下文的概念在JavaScript中非常重要。变量的执行上下文决定了它们可以访问哪些数据,以及它们的行为。每个执行上下文都有一个关联的变量对象,该执行上下文中定义的所有变量和函数都在这个对象上。 全局上下文:全局上下文是最外层的执行上下文。在浏览器中,全局上下文就是window对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法,而使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果相同。 执行上下文在其所有代码都被执行完毕后会被销毁,全局上下文在应用程序退出前才会被销毁,如关闭网页或退出浏览器。 每个函数调用都有自己的执行上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。 上下文中的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象 始终位于作用域链的最前端。 如果上下文是函数,则其活动对象用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。 代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。) 局部作用域中定义的变量可用于在局部上下文中替换全局变量。
注意:函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则。
-
延长作用域链 虽然执行上下文主要有 全局上下文和 函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时: try/catch 语句的catch 块; with 语句; 这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加 指定的对象;对catch 语句而言,则会创建一个新的 变量对象,这个变量对象会包含要抛出的错误对象的声明。 -
没有块级作用域 JavaScript当中没有块级作用域。在其他的类C语言中,由花括号封闭的代码块都有自己的作用域(就是JS中的执行环境),因此可以在其中定义局部变量。而JavaScript当中最小的作用域为函数作用域。
- 声明变量
使用var声明的变量会自动被添加到最接近的环境中。而没有使用var关键字来声明变量时,变量会被添加到全局环境中。 - 查询标识符
在某个环境中引用一个标识符时,必须通过搜索来确定该标识符代表什么意义。执行过程会从当前的作用域链的前端开始,向上逐级查询。如果在局部环境中搜索到了,则该搜索过程就停止。而如果在局部中没有搜索到,则会继续沿作用域链向上搜索,直到全局环境为止。如果全局环境中也没有找到,说明该变量未声明。
3. 引用类型
引用类型的值(对象)是引用类型的一个实例。在ECMAscript中,引用类型是一种数据结构,用于将数据和功能组织在一起。它也常被称为类,但是这种称呼并不妥当。尽管ECMAscript从技术讲是一门面向对象的语言,但是它不具备传统的面向对象语言所支持的类和接口等基本结构。引用类型有时候也称为对象定义,因为它们描述的是一类对象所具有的属性和方法。
3.1 Object
Object 类型在 ECMAScript 中十分常用。虽然 Object 的实例没有多少功能,但很适合存储和在应用程序间交换数据。
创建 Object 类型的实例有两种方法:
- 使用 new 操作符和 Object 构造函数。如:(例1)
ver person = new Object();
person.name = "ZhouHang";
person.age: 29;
- 使用对象字面量表示法。**对象字面量是对象定义的简写形式,目的是为了简化包含大量属性的对象的创建。**如:(例2)
var person = {
name:"ZhouHang",
age:29
}; //此段代码定义了与上例相同的person对象。
- person 后面的赋值操作符表示后面要期待一个值。此时形成了一个表达式上下文。(表达式上下文指的是期待返回值的上下文)
- 左大括号({)出现在表达式上下文中,表示对象字面量开始,即表示一个表达式的开始。(若是 { 出现在语句上下文中,比如 if 语句的条件后面,则表示一个语句块的开始。)
- 逗号用于在对象字面量中分隔属性,但同一个对象字面量中最后一个属性后不写逗号。
- 在对象字面量表示法中,属性名可以是字符串或数值。数值属性会自动转换为字符串。如:(例3)
var person = {
name:"ZhouHang",
age:29,
5:true
};
- 也可以用字面量表示法来定义只有一个默认属性和方法的对象,只需在大括号中写一个空格即可:(例4)
var person = {};
person.name = "ZhouHang";
person.age = 29;
//与例1等效
注意:
- 在使用对象字面量定义对象时,并不会实际调用 Object 构造函数。
- 虽然两种方法都可行,但使用对象字面量表示法代码更少且更加美观。
属性的存取也有两种方法:
- 点语法:(例5)
var person = {
name:"ZhouHnag",
age:29
};
console.log(person.name);
2.中括号语法 使用中括号语法时,要在括号内使用属性名的字符串形式:(例6)
var person = {
name:"ZhouHnag",
age:29
};
console.log(person["name"]);
- 两种存取方式从功能上没有区别,但使用中括号语法可以通过变量访问属性。
- 如果属性名中包含可能会导致语法错误的字符或者包含关键字、保留字时,可以使用中括号语法。如:(例7)
person["first name"] = "ZhouHang";
// 因为"first name"中包含一个空格,所以不能使用点语法来访问。
???(属性名中是可以包含非字母数字字符的,这时候只需要用中括号语法存取就行了)
- 点语法是首选的属性存取方式,除非访问属性时必须使用变量。
3.2 Array
Array 是 ECMAScript 中除 Object 外最常用的类型。 ECMAScript 数组跟其他编程语言的数组有很大区别。虽然也是有序数组,但数组中每个槽位可以存储任意类型的数据。并且 ECMAScript 数组是动态大小的,会随着数据添加而自动增长。
- 创建数组
1. 使用 Array 构造函数: (例8)
var name = new Array();
?
- 如果知道数组元素的数量,则可以给构造函数传入一个值,然后 length 属性会被自动创建并设置为这个值:(例9)
var name = new Array(10); //创建一个初始 length 为10的数组
var name = new Array("ZhouHang","Nicholas","");
注意:创建数组时可以给构造函数传一个值。不过,如果这个值是数值,则会创建一个长度为指定数值的数组;而如果这个值是其它类型的,则会创建一个只包含该特定值的数组。如:(例10)
var name = new Array(8); //创建一个只包含8个元素的数组
var color = new Array("red"); //创建一个只包含一个元素,即字符串"red"的数组
- 使用 Array 构造函数时,也可以省略 new 操作符。
2.使用数组字面量表示法。 数组字面量是指在中括号中包含以逗号分隔的元素列表:(例11)
var colors = Array["red","blue","yellow","purple"]; //创建一个包含3个元素的数组
var names = Array[]; //创建一个空数组
var values = Array[1,2]; //创建一个包含2个元素的数组
var values2 = Array[1,3,]; //创建一个包含2个元素的数组(不推荐这样写)
注意:同对象一样,在使用数字字面量表示法创建数组时不会调用 Array 函数。
3. from ()方法:ES6新增的用于创建数组的静态方法,用于将类数组结构转换为数组实例。 4. of ()方法:ES6新增的第二个用于创建数组的静态方法,用于将一组参数转换为数组实例。
-
数组空位 使用数组字面量初始化数组时,可以使用一串逗号来创建空位。ECMAScript 会将逗号之间相应索引位置的值当成空位。ES6 新增方法会将这些空位当成存在的元素,只是值为 undefined;而 ES6以前的的方法则会忽略这个空位,但具体行为因方法而异。 在实践中应尽量避免使用数组空位。 -
数组索引 要取得或设置数组的值,需要使用中括号并提供相应值的数字索引。在中括号中提供的索引表示要访问的值。如果索引小于数组包含的元素数,则返回存储在相应位置的元素。如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值加1。如:(例12)
var names = ["ZhouHnag","ShiJiu","wang"]; //定义一个字符串数组
alert(names[0]); //显示第一项
names[2] = "Jie"; //修改第三项为"Jie"
names[3] = "wangsjijiu"; //添加第四项"wangshijiu"
- 上例中
alert(names[0];) 表示一个带有一条指定信息(names数组中的第一个元素)和一个 OK 按钮的警示框。alert() 期待字符串。如下图:(例13)  - 数组中元素的数量保存在 length 属性中,这个属性值始终返回0或大于0的值。数组 length属性不是只读的,通过修改 length 属性,可以从末尾删除或添加元素。若果将 length 设置为大于数组元素数的值,则没有被赋予相应值的元素会以 undefined 填充。(例14)
var names = ["ZhouHang","LiNing","cat"];
alert(names[3]); //undefined
alert(names[2]); //cat
names.length = 4; //length值大于数组元素数
alert(names[3]); //undefined
names[names.length] = "wo"; //添加一个元素
alert(names[3]); //undefined
alert(names[4]); //wo
注意:数组最多可以包含4 294 967 295个元素。若尝试添加更多项,则会导致抛出错误;若以这个最大值作为初始值创建数组,则会导致脚本运行时间过长的错误。
- 检测数组
判断一个对象是不是数组,是一个经典的 ECMAScript 问题。在只有一个网页(只有一个全局作用域)的情况下,使用 instanceof 操作符即可。而当网页设计多个不同的全局执行上下文时,就要使用 Array.isArray() 方法,这个方法不关注一个值在哪个全局执行上下文中创建。 语法: instanceof 操作符:
if(value instanceof Array){
//操作数组
}
Array.isArray() 方法:
if(Array.isArray(value)){
//操作数组
}
- 转换方法
对所有对象都有 toLocaleString()、toString() 和 valueOf () 方法:
valueOf() 返回的是数组本身、 toString() 返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串,即对数组的每个值都要调用 toString() 方法,以得到最终的字符串。 toLocaleString() 方法经常也会返回与 toString()和 valueOf()方法相同的值,但也不总是如此。当调用数组的 toLocaleString()方法时,它也会创建一个数组值的以逗号分隔的字符串。而与前两个方法唯一的不同之处在于,这一次为了取得每一项的值,调用的是每一项的 toLocaleString()方法,而不是 toString()方法。
- 数组继承的 toLocaleString()、toString()和 valueOf()方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。而如果使用 join()方法,则可以使用不同的分隔符来构建这个字符串。join()方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。如:(例15)
var colors = ["red", "green", "blue"];
alert(colors.join(",")); //red,green,blue
alert(colors.join("||")); //red||green||blue
若不给 join() 传入任何参数,或者传入 undefined,则仍使用逗号作为分隔符。 注意:如果数组中的某一项的值是 null 或者 undefined,那么该值在 join()、toLocaleString()、toString()和 valueOf()方法返回的结果中以空字符串表示。
- 栈方法
数组对象可以像栈一样,栈是一种先进后出(LIFO)的结构,也就是最近添加的项先被删除。数据项的插入(称为推入)和删除(弹出) 只在栈的一个地方发生,即栈顶。 ECMAScript 数组提供了 push() 和 pop () 方法,以实现类似栈的行为。
- push() 方法接受任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。
- pop() 方法用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的值。
栈方法可以与数组的其他任何方法一起使用。
- 队列方法
队列以先进先出(FIFO)的形式限制访问。在列表末尾添加数据,但从列表开头获取数据。使用 shiift() 和 push(),可以把数组当成队列来使用。shift() 方法会删除数组的第一项并返回它,然后数组长度减一。
ECMAScript 还提供了unshift() 方法,unshift() 即执行跟 shift() 相反的操作**在数组开头添加任意多个值,然后返回新数组的长度。**通过使用 unshift() 和 pop() ,可以在相反方向上模拟队列,即在数组开头添加新数据,在数组末尾取得数据。
- 排序方法
数组有两种方法可以对元素重新排序:reverse() 和 sort(). reverse() 方法 :将数组元素反向排序。 不够灵活,所以有了sort() 方法。 sort() 方法 :按照升序重新排列数组元素。 sort () 会在每一项上调用String() 转型函数,然后比较字符串来决定顺序。 即使数组的元素都是数值,它也会先把数组转换为字符串再比较、排序。这就产生一个问题,数值所对应的字符串大小关系并不一定与数值本身一致。因此,sort() 可以接受一个比较函数,用于判断哪个值应该排在最前面。 比较函数接受两个参数,如果第一个参数应该排在第二个参数的前面,就返回负值;如果两个参数相等,就返回0;如果第一个参数应该排在第二个参数的后面,就返回正值。 (例16)
function compare(value1,value2) {
if(value1 < value2) {
return -1;
} else if(value1 > value2){
return 1;
} else {
return o;
}
}
var values =[2,5,6,3,7];
values.sort(compare);
alert(values);
- 比较函数也可以产生降序效果,只需要把返回值交换一下即可:(例17)
function compare(value1,value2) {
if(value1 < value2) {
return 1;
} else if(value1 > value2){
return -1;
} else {
return o;
}
}
var values =[2,5,6,3,7];
values.sort(compare);
alert(values);
var values = [2,5,6,3,7];
values.sort((a,b) => a < b ? 1 : a > b ? -1 : 0);
alert(values);
如果只是想反转数组的顺序,reverse()更简单也更快。 注意:reverse()和sort()都返回调用它们的数组的引用。
如果数组的元素是数值,或者是其valueOf()方法返回数值的对象(如Date对象),这个比较函数还可以写得更简单,因为这时可以直接用第二个值减去第一个值,比较函数就是要返回小于0、0和大于0的数值,因此减法操作完全可以满足要求。
- 数组方法
数组中的操作元素的方法:
- concat()方法
可在现有数组全部元素基础上创建一个新数组(它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组)。 如果传入一个或多个数组,则concat()会把这些数组的每一项都添加到结果数组。如果参数不是数组,则直接把它们添加到结果数组末尾。 - slice()方法
slice()方法用于 创建 一个包含原有数组中一个或多个元素的新数组,可接收一个或两个参数:返回元素的开始索引和结束索引。 如果只有一个参数,则slice()会返回该索引到数组末尾的所有元素。如果有两个参数,则slice()返回从开始索引到结束索引对应的所有元素(其中不包含结束索引对应的元素)。注意,该操作不影响原始数组。
注意: 如果slice()的参数有负值,那么就以数值长度加上这个负值的结果确定位置。比如,在包含5个元素的数组上调用slice(-2,-1),就相当于调用slice(3,4)。如果结束位置小于开始位置,则返回空数组。
splice()方法的主要目的,是 在数组中间插入元素,但有 3 种不同的方式使用该方法:
- 删除
需要给splice()传 2 个参数:要删除的第一个元素的位置和要删除的元素数量。可以从数组中删除任意多个元素,比如splice(0, 2)会删除前两个元素; - 插入
需要给splice()传 3 个参数:开始位置、0(要删除的元素数量)和要插入的元素,可以在数组中指定的位置插入元素。第三个参数之后还可以传第四个、第五个参数,乃至 任意多个 要插入的元素。比如,splice(2, 0, “red”, “green”)会从数组位置 2 开始插入字符串"red"和"green"。 - 替换
splice()在删除元素的同时可以在指定位置插入新元素,同样要传入3个参数: 开始位置、要删除元素的数量和要插入的任意多个元素。要插入的元素数量不一定跟删除的元素数量一致。比如,splice(2, 1, “red”, “green”)会在位置 2 删除一个元素,然后从该位置开始向数组中插入"red"和"green"。
splice()方法始终返回这样一个数组,它包含从数组中被删除的元素(如果没有删除元素,则返回空数组)。
- 搜索和位置方法
搜索数组的方法有按严格相等搜索和按断言函数搜索。
- 严格相等
ECMAScript提供了 3 个严格相等的搜索方法:
indexOf():从数组第一项开始往后搜索; lastIndexOf():从数组最后一项开始向前搜索; includes():从数组第一项开始往后搜索。
indexOf()和lastIndexOf()都返回要查找的元素在数组中的位置,如果没找到则返回-1。
includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。在比较第一个参数跟数组每一项时,会使用全等(===)比较,也就是说两项必须严格相等。
- 断言函数
ECMAScript 也允许按照定义的断言函数搜索数组,每个索引都会调用此函数。断言函数的返回值决定了相应索引的元素是否被认为匹配。 断言函数接收 3 个 参数:元素、索引 和 数组本身。其中元素是数组中当前搜索的元素,索引是当前元素的索引,数组是正在搜索的数组。断言函数返回真值,表示是否匹配。
find()方法: 返回 第一个匹配的 元素; findIndex()方法:返回 第一个匹配元素的 索引。 【共同点】:
都使用了断言函数; 两个方法都从数组的最小索引开始; 也都接收第 2 个可选参数,用于指定断言函数内部this的值。
- 迭代方法
ECMAScript 为数组定义了 5 个 迭代方法。每个方法接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中this的值)。传给每个方法的函数接收 3 个 参数:数组元素、元素索引 和 数组本身。
数组的 5 个迭代方法:
- every():对数组每一项都运行传入的函数,如果对每一项函数都返回true,则该方法返回true;
- filter():对数组每一项都运行传入的函数,函数返回true的项会组成数组之后返回;
- forEach():对数组每一项都运行传入的函数,没有返回值;
- map():对数组每一项都运行传入的函数,返回 由每次函数调用的结果构成的数;
- some():对数组每一项都运行传入的函数,如果有一项函数返回true,则该方法返回true。
注意: 这 5 个方法都不会改变调用它们的数组。
- 归并方法
ECMAScript 为 数组 提供了两个 归并方法:
- reduce():从数组第一项开始 遍历 到最后一项;
- reduceRight():从最后一项开始 遍历 至第一项。
- 两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。且都接收两个参数:对每一项都会运行的归并函数,以及 可选的以之为归并起点的初始值 。
- 传给reduce()和reduceRight()的并归函数接收4个参数:上一个归并值、当前项、当前项的索引和数组本身。该函数返回的任何值都会作为下一次调用同一个函数的第一个参数。
- 如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。
- 使用reduce()函数执行累加数组中所有数值的操作
- reduceRight()方法与reduce()类似,只是方向相反。
- 究竟是使用reduce() 还是reduceRight() 只取决于遍历数组元素的方向。
3.3 Date
对象被认为是某个特定引用类型的实例。新对象通过使用new操作符后跟一个构造函数来创建。构造函数就是用来创建新对象的函数。
ECMAScript还提供了: Date.now() 方法,返回表示方法执行时日期和时间的毫秒数; Date.parse() 方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。
如果直接把表示日期的字符串传给Date构造函数,那么Date会在后台调用Date.parse()
注意:Date.UTC()也会被Date构造函数隐式调用,但有一个区别:这种情况下创建的是本地日期,不是GMT日期。
3.4 RegExp
ECMAScript通过RegExp类型支持正则表达式。正则表达式使用类似Perl的简洁语法来创建:
let expression = /pattern/flags;
每个正则表达式可以带零个或多个flags(标记),用于控制正则表达式的行为。下面给出了表示匹配模式的标记。
g :全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束i :不区分大小写,表示在查找匹配时忽略pattern和字符串的大小写。 m 多行模式,表示查找到一行文本末尾时会继续查找。y :粘附模式,表示只查找从lastIndex开始及之后的字符串。u :Unicode模式,启用Unicode匹配。s :dotAll模式,表示元字符.匹配任何字符(包括\n或\r)。
RegExp实例的主要方法是exec() ,主要用于配合捕获组使用。这个方法只接收一个参数,即要应用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回null;
正则表达式的另一个方法是test() ,接收一个字符串参数。如果输入的文本与模式匹配,则参数返回true,否则返回false。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。test()经常用在if语句中。 无论正则表达式是怎么创建的,继承的方法toLocaleString() 和toString() 都返回正则表达式的字面量表示。正则表达式的valueOf() 方法返回正则表达式本身。
3.5 原始值包装类型
为了方便操作原始值,ECMAScript提供了 3 种特殊的引用类型:Boolean、Number 和 String。 每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,来暴露操作原始值的各种方法。
实际上,每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。后台过程如下: (1) 创建 String/Boolean/Number 类型的一个实例; (2) 在实例上调用指定的方法; (3) 销毁这个实例 引用类型与基本包装类型的主要区别就是对象的生存期。使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。
- Boolean
Boolean 类型的实例重写了valueOf()方法,返回基本类型值true 或false;重写了toString()方法,返回字符串"true"和"false"。可是,Boolean 对象在ECMAScript 中的用处不大,因为它经常会造成人们的误解。 - String
- 字符串方法:用于访问字符串中特定字符的方法:charAt() 和charCodeAt() 两个方法都接收一个参数,即基于0的字符位置,返回 该位置的字符。
- 字符串操作方法:
concat() 拼接字符串 slice() 分隔字符串, 参数为负值时,将传 入的负值与字符串的长度相加 substr() 两个参数,第一个参数是起始位置(只有一个参数代表返回当前位置到最后的所有字符),第二个参数(可选)是要返回的个数 参数为负值时,将负的第一个参数加上字符串的长度,而将负的第二个 参数转换为 0。 substring() 和slice() 一样,但是参数为负数时,会将所有参数都转换为0. - 字符串位置方法:indexOf() 和lastIndexOf() 和数组的方法类似
- 字符串大小写转换:
toLowerCase() ——转换为小写; toUpperCase() ——转换为大写; toLocaleLowerCase() 和 toLocaleUpperCase()方法则是针对特定地区的实现 - 字符串的模式匹配方法: match()、search()、replace()、 split()
localecompare() 这个方法比较两个字符串,并返回下列 值中的一个: ? 如果字符串在字母表中应该排在字符串参数之前,则返回一个负数(大多数情况下是-1); ? 如果字符串等于字符串参数,则返回 0; ? 如果字符串在字母表中应该排在字符串参数之后,则返回一个正数(大多数情况下是 1)。 fromCharCode()是接收一或 多个字符编码,然后将它们转换成一个字符串
- Number
Number 是与数字值对应的引用类型。要创建 Number 对象,可以在调用 Number 构造函数时向其中传递相应的数值。 Number 类型重写了 valueOf()、toLocaleString()和 toString()方法。重写后的 valueOf()方法返回对象表示的基本类型的数值,另外两个方法则返回字符串形式的数值。 除了继承的方法之外,Number 类型还提供了一些用于将数值格式化为字符串的方法。toFixed()方法会按照指定的小数位返回数值的字符串表示, toExponential()方法返回以指数表示法(也称 e 表示法)表示的数值的字符串形式。
3.6 单例内置对象
内置对象即不用显式实例化内置对象,因为它们已经实例化好了。常见的内置对象包括Object、Array 和 String,这里主要介绍另外两个单例内置对象:Global 和 Math。
- Global对象
Global对象不会被代码显式访问,ECMA-262规定Global对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。
事实上,不存在 全局变量 或 全局函数 这种东西。在全局作用域中定义的变量和函数都会变成Global对象的属性 。 包括isNaN()、isFinite()、parseInt()和parseFloat(),实际上都是 Global 对象的方法。除了这些,Global 对象上还有另外一些方法。
- URL编码方法
用于编码统一资源标识符(URI),以便传给浏览器。有效的URI不能包含某些字符(比如空格)。使用URI编码方法来编码URI可以让浏览器能够理解它们,同时又以特殊的UTF-8编码替换掉所有无效字符。
编码方法: ecnodeURI() 方法:用于对整个URI进行编码;比如 “www.wrox.com/illegal value.js”。 encodeURIComponent() 方法:用于编码URI中单独的组件,比如前面URL中的 " illegal value.js "
let uri = "http://www.wrox.com/illegal value.js#start";
console.log(encodeURI(uri));
console.log(encodeURIComponent(uri));
encodeURI()不会编码属于URL组件的特殊字符(比如冒号、斜杠、问号、井号),而encodeURIComponent()会编码它发现的所有非标准字符。
注意: 一般来说,使用 encodeURIComponent() 比使用 encodeURI() 的频率更高,这是因为编码查询字符串参数比编码基准URI的次数更多。
与encodeURI()和encodeURIComponent()相对的是decodeURI() 方法和decodeURIComponent() 方法:
decodeURI() 方法:只对使用encodeURI()编码过的字符解码。例如,%20会被替换为空格,但%23不会被替换为井号(#),因为井号不是由encodeURI()替换的。 decodeURIComponent() 方法:解码所有被encodeURIComponent()编码的字符,基本上就是解码所有特殊值。
- eval方法
eval() 可能是整个ECMAScript语言中最强大的了,它就是一个完整的 ECMAScript 解释器 。
eval("console.log('hi')");
`等价于
console.log("hi");
通过eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。定义在包含上下文中的变量可以在eval()调用内部被引用。
let msg = "hello world";
eval("console.log(msg)");
也可以在eval()内部定义一个函数或变量,然后在外部代码中引用。
eval("function sayHi() { console.log('hi'); }");
sayHi();
通过eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。只是在eval()执行的时候才会被创建。
注意: 1.严格模式下,在eval()内部创建的变量和函数无法被外部访问。同样,在严格模式下,赋值给eval也会导致错误; 2.解释代码字符串的能力虽然非常强大,但也很危险。在使用eval()时必须慎重,特别是解释用户输入内容时。因为该方法会对XSS 利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。
- Global对象属性
 - window 对象
浏览器将 window 对象实现为 Global 对象的代理。故所有全局作用域中声明的变量和函数都变成了 window 的属性 。 另一种获取 Global 对象的方式,如下所示:
let global = function() {
return this;
}();
本段代码创建了立即函数,返回了this的值。 当一个函数在没有明确(通过成为某个对象的方法,或者通过call()/apply() )指定this值的情况下执行时,this值等于 Global 对象。 因此,调用一个简单返回this的函数是在任何执行上下文中获取 Global 对象的通用方式。
- Math
Math 对象用于保存数学公式、信息和计算,并提供了一些辅助计算的属性和方法。
Math对象上提供的计算要比直接在 JavaScript 实现的快得多,因为Math对象上的计算使用了JavaScript 引擎中更高效的实现和处理器指令。
注意:使用 Math 计算的精度会因浏览器、操作系统、指令集和硬件而异 。
- Math对象属性
 - min() 和 max() 方法
用于确定一组数值中的最小值和最大值,都接收任意多个参数。
let max = Math.max(3, 54, 32, 16);
console.log(max);
let min = Math.min(3, 54, 32, 16);
console.log(min);
这两个方法可以避免使用额外的循环和if语句来确定一组数值的最大、最小值。 数组中的最大值和最小值,可以像下面这样使用扩展操作符(... ):
let values = [1, 2, 3, 4, 5, 6, 7, 8];
let max = Math.max(...values);
Math.ceil() 方法:向上舍入 为最接近的整数;Math.floor() 方法:向下舍入 为最接近的整数。;Math.round() 方法:执行 四舍五入;Math.fround() 方法:返回数值最接近的 单精度(32位)浮点值。
- 6.2.4 Math.random()方法
Math.random() 方法返回一个0~1范围内的随机数,其中包含0但不包含1。可应用于显示随机名言或随机新闻的网页。 从一组整数中随机选择一个数:
1)公式:
number = Math.floor(Math.random() * total_number_of_choices + first_possible_value)
公式中使用了Math.floor()方法,因为Math.random()始终返回小数,即便乘以一个数再加上一个数也是小数。 2)代码:
从1~10范围内随机选择一个数,代码如下:
let num = Math.floor(Math.random() * 10 + 1);
这样就有 10 个可能的值(1~10),其中最小的值为1。 如果想选择一个2~10范围内的值,则代码如下:
let num = Math.floor(Math.random() * 9 + 2);
2~10只有9个数,所以可选总数(total_number_of_choices)是9,而最小可能的值(first_possible_value)为2。
- 其他方法

4. 对象
4.1 理解对象
- 属性类型
ECMAScript 中有两种属性:数据属性和访问器属性 1).数据属性 数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性: Configurable - 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特征,或者能否把属性修改为访问器属性,默认值为true Enumerable - 示能否通过for-in循环返回属性,默认值为true Writeable - 能否写入 Value - 包含这个属性的数据值,写入属性的时候把值存在这,默认值为 undefined Object.defineProperty() - 修改默认特性 如:
var person = {}
Object.defineProperty(person, "name", {
writable: false,
value: "limy"
})
console.log(person.name);
person.name = "limy1"
console.log(person.name);
?????????2).访问器属性 Configurable - 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特征,或者能否把属性修改为访问器属性,默认值为true; Enumerable - 示能否通过for-in循环返回属性,默认值为true; Get - 读取属性时调用的函数; Set - 写入属性时调用的函数; 访问器属性不能直接定义,需要通过Object.defineProperty() 如:
var book = {
_year: 2018,
edition: 1
};
Object.defineProperty(book,"year", {
get: function() {
return this._year
},
set: function(newValue) {
if(newValue > 2018) {
this._year = newValue;
this.edition += newValue - 2018;
}
}
});
book.year = 2019;
alert(book.edition);
-
定义多个属性 ECMAScript 5 定义了一个 Object.defineProperties() 方法。利用这个方法可以通过描述符一次定义多个属性。 这个方法接收两个对象参数:第一对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。 支持 Object.defineProperties()方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。 -
读取属性的特性 使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。 这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这个对象的属性有 configurable、enumerable、writable 和 value。
在 JavaScript 中,可以针对任何对象——包括 DOM 和 BOM 对象,使用 Object.getOwnPropertyDescriptor()方法。支持这个方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome
4.2 创建对象
-
工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。即创建了一种函数,把对象放到函数里,用函数封装创建对象的细节。 工厂模式解决了代码复用(即创建多个类似对象)的问题,却没有解决对象识别的问题。创建的所有对象都是Object 类型。为了解决这一问题,又有了构造函数模式。 -
构造函数模式 我们知道,ECMAScript 中的构造函数可用来创建特定类型的对象。像 Object 和 Array 这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。 构造函数与普通函数的唯一区别就是调用方式不同,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。 构造函数的首字母名称都是要大写的,非构造函数则以小写字母开头。如此可以更好地辨别二者。 使用 new 操作符调用构造函数会执行以下操作:
- 在内存中创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式相较于工厂模式的优点。
构造函数定义的方法会在每个实例上都创建一遍,这会导致相同的函数会被重复定义。这个问题可以通过把函数定义到构造函数外部来解决。但是这个解决办法虽然解决了相同逻辑的函数重复定义的问题,却也打乱了全局作用域。所以又产生了原型模式来解决这一问题。
- 原型模式
每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。即 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。 使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。即不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
- 理解原型对象
无论何时,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。 在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。 创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫[[Prototype]] 。虽然在脚本中没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__ (_proto_属性用来读取或设置当前对象的prototype 属性);而在其他实现中,这个属性对脚本则是完全不可见的。不过,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf() 方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用 isPrototypeOf()方法的对象,那么这个方法就返回 true
ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf() ,在所有支持的实现中,这个方法返回[[Prototype]]的值。 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。 如:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name);
alert(person2.name);
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。 如:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name);
alert(person2.name);
delete person1.name;
alert(person1.name);
使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法是从 Object 继承来的,故只在给定属性存在于对象实例中时,才会返回 true。通过使用 hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了。
ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法。
- 原型与
in 操作符 有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。
在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。 无论属性存在于实例中还是存在于原型中,只要同时使用 hasOwnProperty() 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。 因为 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于实例中时才返回 true,故只要 in 操作符返回 true 而hasOwnProperty()返回 false,就可以确定属性是原型中的属性。
在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]] 标记为 false 的属性)的实例属性也会在 for-in 循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外 (IE 早期版本的实现中存在一个 bug,即屏蔽不可枚举属性的实例属性不会出现在 for-in 循环中)。
要取得对象上所有可枚举的实例属性,可使用ES5的object.keys() 方法。该方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
function Person(){};
Person.prototype.name = 'Lily';
Person.prototype.age = 17;
Person.prototype.job = 'Teacher';
Person.prototype.sayName = function(){
alert(this.name);
}
var keys = Object.keys(Person.prototype);
alert(keys);
alert(Array.isArray(keys));
alert(keys.length);
var person1 = new Person();
person1.name = "Candy";
person1.job = "Singer";
var keys2 = Object.keys(person1);
alert(keys2);
constructor 属性不可枚举。若想获得所有实例属性(无论是否可枚举),则可使用Object.getOwnPropertyName() 方法。
function Person(){};
Person.prototype.name = 'Lily';
Person.prototype.age = 17;
Person.prototype.job = 'Teacher';
Person.prototype.sayName = function(){
alert(this.name);
}
var keys = Object.getOwnPropertyName(Person.prototype);
alert(keys);
将 Person.prototype 设置为等于一个以对象字面量创建的新对象,是更为简单的原型语法。但这样的语法,本质上是完全重写了默认的 prototype 对象,使得 constructor 属性不再指向Person了(指向Object构造函数)。 可通过以下方式将constructor属性重新指向Person:
function Person(){};
Person.prototype = {
constructor: Person,
name: 'Lily',
age: 17,
job: 'Teacher',
sayName: function(){
alert(this.name);
}
};
但这种方式会导致它的 [[Enumerable]](可枚举)特性被设置为true。
对原型对象所做的任何修改都能立即从实例上反映出来,即便是先创建了实例后修改原型:
var friend = new Person();
Person.prototype.sayHi = function(){
alert('Hi!');
};
friend.sayHi();
但若是重写整个原型对象,情况就不一样了:
var friend = new Person();
Person.prototype = {
construcor: Person,
name: 'Candy',
age: 22,
job: 'Dancer',
sayName: function(){
alert(this.name);
}
};
friend.sayName();
因为,调用构造函数时会为实例添加一个指向最初原型的指针,而把原型修改为另外一个对象就等同于切断了实例与最初原型之间的联系。 实例中的指针仅指向原型,而不指向构造函数。 重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系,他们引用的仍然是最初的原型。
原型对象的弊端:当属性中包含引用类型值时,修改实例中相应的引用类型值将会间接修改原型中的引用类型值。
- 组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数。
这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
- 动态原型模式
动态原型是把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型,如:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var friend = new Person("Tony",21,"teacher");
friend.sayName();
- sayName()方法不存在的情况下,才会将他添加到原型中。
- 这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了,不过这里对原型所做的修改,能够立即在所有实例中得到反映,因此,这种方法确实so good。
- 其中,if语句检查的可以是初始化之后应该存在的任何属性和方法——不必用一大堆if语句检查每个属性和方法,只要检查其中一个即可。
对于才用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。 注意:使用动态原型模式时,不能使用对象字面量重写原型,如果在已经创建了实例的情况下重写原型,就会切断现有实例和原型之间的联系。
- 寄生构造函数
在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。寄生构造函数的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。如:
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。
除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。这个模式可以在特殊的情况下用来为对象创建构造函数。若想创建一个具有额外方法的特殊数组,又由于不能直接修改 Array 构造函数,因此可以使用这个模式。
关于寄生构造函数模式,需要注意的是:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系,即也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情 况下,不要使用这种模式。
- 稳妥构造函数模式
道格拉斯·克罗克福德(Douglas Crockford)发明了 JavaScript 中的稳妥对象(durable objects)这个概念。稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。 稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup程序)改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
- 新创建对象的实例方法不引用 this;
- 二是不使用 new 操作符调用构造函数。
4.3 继承
继承是 OO 语言中的一个最为人津津乐道的概念。许多 OO 语言都支持两种继承方式:接口继承和实现继承。
接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
- 原型链
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。故而,使原型对象等于另一个类型的实例,就实现继承:
A.prototype = new B();
假如让原型对象等于另一个类型的实例。则此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
原型链继承的实质就是重写原型对象。
当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,若没有找到该属性,则会继续搜索实例的原型。在通过原型链继承的情况下,搜索过程就得以沿着原型链继承向上。
所以引用类型都默认继承了Object,这个继承也是通过原型链实现的。所以函数的1默认原型都是Object的实例。P164
确定原型和实例之间的关系:使用 instanceof 和 isPrototypeOf() 方法。
alert(A instanceof B);
alert(A.prototype.isPrototypeOf(B));
注:
- 给原型添加方法的代码一定要放在替换原型的语句之后。
- 在通过原型链实现继承时,不能使用对象字面量创建原型方法,这样会重写原型方法。
- 原型链继承的弊端:问题源于包含引用类型值的原型,包含引用类型值的原型属性会被所有实例共享。
原型链的问题:
- 最主要的问题来自包含引用类型值的原型。在原型模式中学到了包含引用类型值的原型属性会被所有实例共享;这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。
- 借助构造函数
在解决原型中包含引用类型值所带来的问题的过程中,开发人员使用一种叫做借用构造函数的技术(有时也叫做伪造对象或经典继承)。借用构造函数的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。因为函数只不过是在特定环境中执行代码的对象,因此通过使用apply() 和call() 方法也可以在新创捷的对象上执行构造函数:
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);
var instance2 = new SubType();
alert(instance2.colors);
- 传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name){
this.name = name;
}
function SubType(){
SuperType.call(this, "Nicholas");
this.age = 29;
}
var instance = new SubType();
alert(instance.name);
alert(instance.age);
SuperType 只接受一个参数 name,该参数会直接赋给一个属性。在 SubType 构造函数内部调用 SuperType 构造函数时,实际上是为 SubType 的实例设置了 name 属性。为了确保SuperType 构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此就无法复用函数了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。因此借用构造函数的技术也很少单独使用。
-
组合继承 组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。 缺点:无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。 -
原型式继承 借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o){
function F(){};
F.prototype = 0;
return new F();
}
在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的 原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给 object() 函数,然后再根据具体需求对得到的对象加以修改即可。 ECMAScript 5 通过新增 Object.create (对象,(可选)新对象定义额外属性的对象)方法规范化了原型式继承。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
注意:在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。 缺点:包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
- 寄生式继承
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
- 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
- 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
- 寄生组合式继承
虽然 组合继承 被认为是JavaScript 中最常用的集成模式,但它仍然有一个问题:就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。对于这个问题,解决办法就是使用寄生组合式继承。 寄生组合式继承,就是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。 本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。 寄生组合式继承的基本模式如下所示:
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
优点:效率高,值调用一次超类型原型的构造函数,原型链还能保持不变,能正常使用instanceof和isPrototypeOf( )。是最理想的继承范式。
5. 函数表达式
定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。
- 函数声明
函数声明的语法是这样的。
function functionName(arg0, arg1, arg2) {
}
首先是 function 关键字,然后是函数的名字,这就是指定函数名的方式。Firefox、Safari、Chrome和 Opera 都给函数定义了一个非标准的name 属性,通过这个属性可以访问到给函数指定的名字。这个属性的值永远等于跟在 function 关键字后面的标识符。
alert(functionName.name);
函数声明的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。
- 函数表达式
函数表达式有几种不同的语法形式。下面是最常见的一种形式。
var functionName = function(arg0, arg1, arg2){
};
这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 functionName。这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。(匿名函数有时候也叫拉姆达函数。)匿名函数的 name 属性是空字符串。 函数表达式与其他表达式一样,在使用前必须先赋值。
5.1 递归
函数的递归:一个函数在执行过程中调用自身。 一个经典的递归阶乘函数:
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
虽然这个函数表面看来没什么问题,但下面的代码却可能导致它出错:
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4));
以上代码先把 factorial()函数保存在变量 anotherFactorial 中,然后将 factorial 变量设置为 null,结果指向原始函数的引用只剩下一个。但在接下来调用 anotherFactorial()时,由于必须执行 factorial(),而 factorial 已经不再是函数,所以就会导致错误。在这种情况下,使用 arguments.callee 可以解决这个问题。
arguments.callee 是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用。如:
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
加粗的代码显示,通过使用 arguments.callee 代替函数名,可以确保无论怎样调用函数都不会出问题。因此,在编写递归函数时,使用 arguments.callee 总比使用函数名更保险。 但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误。不过,可以使用命名函数表达式来达成相同的结果。例如:
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});
以上代码创建了一个名为 f()的命名函数表达式,然后将它赋值给变量 factorial。即便把函数赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。
5.2 闭包
首先要明确区分匿名函数和闭包是不同的概念。 闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数的内部创建另一个函数。
要理解闭包,还要理解作用域链的概念。
变量对象:后台的每个执行环境都有一个表示变量的对象——变量对象。
Scope属性:保存了外部环境中的作用域链。
作用域链的本质:一个指向变量对象的指针列表,它只引用但不包含变量对象。
一般说来,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保留全局作用域(全局执行环境的变量对象)。
但是,对于闭包来说,在闭包还未销毁之前,其所在的外部函数在执行完毕后,这个外部函数的执行环境的作用域链会被销毁,但是其活动对象依然保留在内存中;直到闭包销毁后,闭包的外部函数的活动对象才会被销毁。
由于闭包会携带包含它的函数作用域,因此会占用比其他函数更多的内存。过度使用闭包会导致内存占用过多,除非绝对必要,不要使用闭包。
- 闭包与变量
作用域链的这种配置机制引出了一个副作用,即闭包只能取得包含函数中任何变量的最后一个值(闭包所保存的是整个变量对象,而不是某个特殊的变量。)
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
我们可以通过创建另一个匿名函数强制让闭包的行为符合预期:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
}
- 关于this对象
在闭包中使用this对象可能会导致一些问题。 this对象是在运行时基于函数的执行环境绑定的:
- 在全局函数中,this等于window;
- 而当函数被作为某个对象的方法调用时,this等于那个对象。
不过,匿名函数的执行环境具有全局性,因此其this对象通常是指window(还有一种情况就是使用 call() 或 apply() 改变了函数执行环境)。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。例:
var name = "this is my name";
var object = {
name: "Ethan",
getNameFunc: function () {
return function () {
return this.name;
};
}
};
alert(object.getNameFunc()());
- 注:alert里面的函数有两个括号:
第一个括号执行getNameFunc()方法,返回了一个匿名函数,如果没有第二个括号警告框将打印出匿名函数的内容(即getNameFunc()方法中return之后的内容); 如果有第二个括号,说明执行了匿名函数,这是匿名函数是在全局环境下执行的,因此会在全局中查找名为name的属性并返回,在警告框中打印出来。
要访问外部函数中的活动对象,在外部函数中需要定义一个变量保存,而不是使用this。 arguments也是如此,如果想在闭包中访问外部函数中的arguments而不是另外一个运行环境中的arguments,就把定义时外部函数的arguments引用保存到另一个变量中,在闭包中使用这个变量访问包含闭包的外部函数的arguments。
- 内存泄漏
由于IE9之前的版本对Jscript对象和COM对象使用了不同的垃圾回收例程,因此闭包在IE的一些低版本中就存在一些问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁。如:
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
解决的方法就是用另一个变量保存闭包中引用的值(这样做可以消除循环引用):
function assignElement() {
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function () {
alert(id);
};
element = null;
}
这样闭包的外部函数就可以将element的值置为null以便回收内存,闭包保存的外部函数的
活动对象element的值为null,最终会被回收。
但是仅仅做到这一步,还是无法解决内存泄漏的问题。
5.3 模仿块级作用域
JavaScript 没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。如下:
function outputNumbers(count) {
for(var i=0; i<count; i++) {
alert(i);
}
alert(i);
}
JavaScript从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见(不过,他会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。
用作块级作用域(通常称为私用作用域)的匿名函数的语法如下所示:
(function()) {
})();
以上代码定义并立即调用了一个匿名函数。 函数表达式的后面可以跟圆括号。因此,这里通过给函数声明加上一对圆括号将其转换成函数表达式。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:
function outputNumbers(count) {
(function() {
for(var i=0; i<count; i++) {
alert(i);
}
})();
alert(i);
}
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个有很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。例:
<font size=5>(function(){
var now = new Date();
if (now.getMonth() == 0 && now.getDate() == 1){
alert("Happy new year!");
}
})();
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
5.4 私有变量
严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。 私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
如果在一个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问该函数内部的变量。而利用这一点,就可以创建用于访问私有变量的公有方法。 我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。有两种在对象上创建特权方法的方式。
- 第一种是在构造函数中定义特权方法,基本模式如下:
function MyObject(){
var privateVariable = 10;
function privateFunction(){
return false;
}
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
这个模式在构造函数内部定义了所有私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。对这个例子而言,变量 privateVariable 和函数 privateFunction()只能通过特权方法 publicMethod()来访问。在创建 MyObject 的实例后,除了使用 publicMethod()这一个途径外,没有任何办法可以直接访问privateVariable 和 privateFunction()。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据,例如:
function Person (name) {
this.getName = function () {
return name;
};
this.setName = function (value) {
name = value
}
}
构造函数中定义特权方法也有一个缺点,针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法可以避免这个问题。
- 静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下所示:
(function(){
var privateVariable = 10;
function privateFunction(){
return false;
}
MyObject = function(){
};
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();
这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了私有变量和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式,函数声明只能创建局部函数。出于同样的原因,我们也没有在声明 MyObject 时使用 var 关键字。因为初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。但是,在严格模式下给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。
多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个显明的不足之处。
- 模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式(module pattern)则是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。按照惯例,JavaScript 是以对象字面量的方式来创建单例对象的。例:
var singleton = {
name: value,
method : function () {
}
};
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
var singleton = function(){
var privateVariable = 10;
function privateFunction(){
return false;
}
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是 Object 的实例,因为最终要通过一个对象字面量来表示它。事实上,这也没有什么;毕竟,单例通常都是作为全局对象存在的,我们不会将它传递给一个函数。因此,也就没有什么必要使用instanceof 操作符来检查其对象类型了。
- 增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。例:
var singleton = function() {
var privateVariable = 10;
function privateFunction() {
retur false;
}
var object = new CustomType();
object.publicProperty = true;
object.publicMethod = function() {
privateVariable++;
return privateFunction();
};
return object;
}();
如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,那么就可以使用以下代码。
var singleton = function () {
var privateVariable = 10;
function privateFunction() {
return false;
}
return {
publicProperty : true,
publicMethod : function () {
privateVariable++;
return privateFunction();
}
};
}();
C语言文件操作
1.数据流与缓冲区
1.1数据流
就C程序而言,从程序移进,移出字节,这种字节流就叫做流。程序与数据的交互是以流的形式进行的。进行C语言文件的读写时,都会先进行“打开文件”操作,这个操作就是在打开数据流,而“关闭文件”操作就是关闭数据流。
1.2缓冲区
在程序执行时,所提供的额外内存,可用来暂时存放准备执行的数据。它的设置是为了提高存取效率,因为内存的存取速度比磁盘驱动器快得多。 当使用标准I/O函数(包含在头文件stdio.h中)时,系统会自动设置缓冲区,并通过数据流来读写文件。当进行文件读取时,是先打开数据流,将磁盘上的文件信息拷贝到缓冲区内,然后程序再从缓冲区中读取所需数据。事实上,当写入文件时,并不会马上写入磁盘中,而是先写入缓冲区,只有在缓冲区已满或“关闭文件”时,才会将数据写入磁盘。
2.文件类型
文本文件和二进制文件:
文本文件是以字符编码的方式进行保存的。
二进制文件将内存中的数据原封不动的进行保存,适用于非字符为主的数据。其实,所有的数据都可以算是二进制文件。二进制文件的优点在于存取速度快,占用空间小。
3.文件存取方式
顺序存取方式和随机存取方式:
顺序存取就是从上往下,一笔一笔读取文件的内容。写入数据时,将数据附加在文件的末尾。这种存取方式常用于文本文件。 随机存取方式多半以二进制文件为主。它会以一个完整的单位来进行数据的读取和写入,通常以结构为单位。
4. 文件操作函数
C语言中没有输入输出语句,所有的输入输出功能都用 ANSI C提供的一组标准库函数来实现。文件操作标准库函数有:
- 文件的打开
fopen():打开文件 - 文件的关闭
fclose():关闭文件 - 文件的读写
fgetc():读取一个字符 fputc():写入一个字符 fgets():读取一个字符串 fputs():写入一个字符串 fprintf():写入格式化数据 fscanf():格式化读取数据 fread():读取数据 fwrite():写入数据 - 文件状态检查
feof():文件是否结束 ferror():文件读/写是否出错 clearerr():清除文件错误标志 ftell():文件指针的当前位置 - 文件指针定位
rewind():把文件指针移到开始处 fseek():重定位文件指针 参数如下:
“r” :以只读的形式打开文本文件(不存在则出错) “w” :以只写的形式打开文本文件(若不存在则新建,反之,则从文件起始位置写,覆盖原内容) “a” :以追加的形式打开文本文件(若不存在,则新建;反之,在原文件后追加) “r+” :以读写的形式打开文本文件(读时,从头开始;写时,新数据只覆盖所占的空间) “wb”:以只写的形式打开二进制文件 “rb” :以只读的形式打开二进制文件 “ab” :以追加的形式打开一个二进制文件 “rb+” :以读写的形式打开二进制文件。 “w+” :首先建立一个新文件,进行写操作,然后从头开始读(若文件存在,原内容将全部消失) “a+” :功能与”a”相同。只是在文件尾部追加数据后,可以从头开始读 “wb+” :功能与”w+”相同。只是在读写时,可以由位置函数设置读和写的起始位置 “ab+” :功能与”a+”相同。只是在文件尾部追加数据之后,可以由位置函数设置开始读的起始位置
- 打开文件
FILE *fopen( const char *filename, const char *mode ); filename:文件的路径 mode:打开模式
例:
int main()
{
FILE* f;
f = fopen("file.txt", "w");
if (f != NULL)
{
fputs("fopen example", f);
fclose(f);
f=NULL;
}
return 0;
}
注意: 文件是否打开成功 关闭文件 文件指针置空
- 关闭文件
函数原型:int fclose( FILE *stream ); stream:流
例:
if(fclose(f)!=0)
{
printf("File cannot be closed/n");
exit(1);
}
else
{
printf("File is now closed/n");
}
- 读取字符
int fgetc ( FILE * stream ); stream:流
例:
#include <stdio.h>
int main ()
{
FILE * pFile;
int c;
int n = 0;
pFile = fopen ("D:\\myfile.txt", "r");
if (pFile == NULL) perror ("Error opening file");
else
{
while (c != EOF)
{
c = fgetc (pFile);
if (c == '$') n++;
}
fclose (pFile);
printf ("The file contains %d dollar sign characters ($).\n",n);
}
return 0;
}
- 写入字符
int fputc( int c, FILE *stream ); c:要写入的字符 stream:流
例:
char ch;
FILE* pf = fopen("file.txt", "w");
if (pf == NULL)
{
perror("error opening file");
exit(0);
}
ch = getchar();
while (ch != '$')
{
fputc(ch, pf);
ch = getchar();
}
fclose(pf);
- 读取字符串
char * fgets ( char * str, int num, FILE * stream );
str:将读取到的内容复制到的目标字符串 num:一次读取的大小 stream:流 例:
char buf[10] = { 0 };
FILE *pf = fopen("file.txt", "r");
if (pf == NULL)
{
perror("open file for reading");
exit(0);
}
fgets(buf, 9, stdin);
printf("%s", buf);
fclose(pf);
- 写入字符串
int fputs( const char *string, FILE *stream ); string:要写入的字符串 stream:一次读取的大小
例:
char buf[10] = { 0 };
FILE *pf = fopen("file.txt", "r");
if (pf == NULL)
{
perror("open file for reading");
exit(0);
}
fgets(buf, 9, stdin);
fputs(buf, stdout);
fclose(pf);
- 读取数据块
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); ptr:目标内存块 size:一次读取的字节大小 count:一次读取多少个 size stream:流
例:
#include <stdio.h>
#include <string.h>
int main()
{
FILE *pFile = fopen("file.txt", "rb");
if (pFile == NULL)
{
perror ("Error opening file");
return 0;
}
char buf[100] = { 0 };
while (!feof(pFile))
{
memset(buf, 0, sizeof(buf));
size_t len = fread(buf, sizeof(char), sizeof(buf), pFile);
printf("buf: %s, len: %d\n", buf, len);
}
fclose(pFile);
}
-
写入数据块 size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ); 同理,简单好理解,就不详细阐述了。 -
文件指针重定位 int fseek ( FILE * stream, long int offset, int origin ); stream:流 offset:相对应 origin 位置处的偏移量,单位为字节 origin:指针的位置 #define SEEK_CUR 1 // 当前位置 #define SEEK_END 2 // 末尾 #define SEEK_SET 0 // 开头 -
获取指针位置 long int ftell ( FILE * stream ); stream:流 -
获取文件大小
例:
long n;
fseek(pf,0,SEEK_END);
n=ftell(pf);
- 文件指针移到开始处
void rewind( FILE *stream ); stream:流 - 清除文件错误标志
void clearerr( FILE *stream ); stream:流 - 文件流是否读到了文件尾
int feof( FILE *stream ); stream:流 - 重命名文件
int rename ( const char * oldname, const char * newname ); oldname:原名 newname:新名 - 删除文件
int remove ( const char * filename ); filename:文件的路径
|