1.<script>
元素中属性
async:表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效。
defer:表示脚本可以延迟到文档完全被解析和显示之后再操作。只对外部脚本文件有效。IE7及更早版本对嵌入脚本也支持这个属性。
async与defer不同的是,并不保证按照指定它们的先后顺序执行。
2.var声明的局部变量(作用域)。
3.可以使用一条语句定义多个变量,用逗号分隔开。
var message=“hi”,
found=false,
age=29;
4.数据类型
5种简单数据类型(基本数据类型)
Undefined、Null、Boolean、Number、String
1种复杂数据类型
Object
5.typeof操作符
检测给定变量的数据类型
"undefined"----如果这个值未定义
"boolean"----如果这个值是布尔值
"string"----如果这个值是字符串
"number"----如果这个值是数值
"object"----如果这个值是对象或null
"function"----如果这个值是函数
6.Undefined类型
Undefined类型只有一个值,即特殊的undefined。在使用var声明变量但未对其加以初始化时,这个变量的值就是undefined。
例如:
var message;
alert(message==undefined);// true
var message=undefined;
alert(message==undefined);// true
这里使用undefined值显式初始化变量message,但是没有必要这么做,未经初始化的值默认就会取得undefined值。
var message;
alert(message);//"undefined"
alert(typeof message);//"undefined"
alert(age);//产生错误
alert(typeof age);//"undefined"
对未初始化和未声明的变量执行typeof操作符都返回undefined值。
7.Null类型
Null类型是第二个只有一个值的数据类型,这个特殊的值是null。从逻辑角度来看,null值表示一个空对象指针,而这也正是使用typeof操作符检测null值时会返回"object"的原因。
var car=null;
alert(typeof car);//"object"
alert(null==undefined);// true
alert(null===undefined);//false
undefined值是派生自null值的,因此ECMA-262规定对它们的相等性测试返回true。
尽管null和undefined有这样的关系,但它们的用途完全不同。无论在什么情况下都没有必要把一个变量的值显式的设置为undefined,可是同样的规则对null却不适用。只要意在保存对象的变量还没有真正保存对象,就应该明确的让该变量保存null值。这样做不仅可以体现null作为空对象指针的惯例,而且也有助于进一步区分null和undefined。
8.Boolean类型
需要注意的是,Boolean类型的字面量true和false是区分大小写的。也即,True和False(以及其他的混合大小写形式)都不是Boolean值,只是标识符。
要将一个值转换为其对应的Boolean值,可以调用转型函数Boolean()。
各种数据类型及其对应的转换规则
9.Number类型
正零(+0)和负零(-0),被认为相等
浮点数值
if(a+b==0.3){// 不要做这样的测试
alert("You got 0.3");
}
在这个例子中,测试两个数的和是不是等于0.3.如果这两个数是0.05和0.25,或是0.15和0.15都不会有问题。如果是0.1和0.2,测试将无法通过。因此,永远不要测试某个特定的浮点数值。
关于浮点数值计算会产生舍入误差的问题,有一点需要明确:这是使用基于IEEE754数值的浮点计算的通病,ECMAScript并非独此一家;其他使用相同数值格式的语言也存在这个问题。
NaN
NaN,即非数值(Not a Number)是一个特殊的数值。这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误)。在其他编程语言中,任何数值除以非数值都会导致错误,从而停止代码执行。但在ECMAScript中,任何数值除以非数值会返回NaN,不会影响其他代码的执行。
NaN的特点:
(1)任何涉及NaN的操作都会返回NaN;
(2)NaN与任何值都不相等,包括NaN本身。
alert(NaN==NaN);//false
isNaN(),任何不能被转换为数值的值都会导致整个函数返回true。
alert(isNaN(NaN));// true
alert(isNaN(10));// false(10是一个数值)
alert(isNaN("10"));//false(可以被转换为数值10)
alert(isNaN("blue"));//true(不能被转换为数值)
alert(isNaN(true));//false(可以被转换为数值1)
数值转换
有3个函数可以把非数值转换为数值:Number()、parseInt()和parseFloat()。转型函数Number()可以用于任何数据类型,而另外两个函数则专门用于把字符串转换成数值。
Number()函数的转换规则:
(1)如果是Boolean值,true和false将分别被转换为1和0;
(2)如果是数字值,只是简单的传入和返回;
(3)如果是null值,返回0;
(4)如果是undefined,返回NaN;
(5)如果是字符串,遵循下列规则:
①如果字符串中只包含数字(包括前面带正号或负号的情况),则将其转换为十进制数值;
②如果字符串中包含有效的浮点格式,则将其转换为对应的浮点数值;
③如果字符串中包含有效的十六进制格式,例如“0xf”,则将其转换为相同大小的十进制整数值;
④如果字符串是空的(不包含任何字符),则将其转换为0;
⑤如果字符串中包含除上述格式之外的字符,则将其转换为NaN.
(6)如果是对象,则调用对象的valueof()方法,然后依照前面的规则转换返回的值。如果转换的结果是NaN,则调用对象的toString()方法,然后再次依照前面的规则返回字符串值。
用parseInt()转换空字符串会返回NaN.
10.垃圾收集
标记清除
JS中最常用的垃圾收集方式是标记清除。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”,当变量离开环境时,则将其标记为“离开环境”。
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用计数就是1.如果同一个值又被赋给另一个变量,则该值的引用计数加1.相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用计数减1.当这个值的引用计数变为0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
遇到的严重问题----循环引用。
11.Object引用类型
对象是某个特定引用类型的实例。新对象是使用new操作符后跟一个构造函数来创建的。构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。
创建Object实例的方式有两种。第一种是使用new操作符后跟Object构造函数:
var person=new Object();
person.name="Nicholas";
person.age=29;
另一种方式是使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含属性的对象的过程:
var person={
name:"Nicholas",
age:29
}
12.点表示法、方括号表示法
一般来说,访问对象属性时使用的都是点表示法,这也是很多面向对象语言中通用的语法。不过,在JS也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要访问的属性以字符串的形式放在方括号中。
alert(person["name"]);
alert(person.name);
从功能上看,这两种访问对象属性的方法没有任何区别,但方括号语法的主要优点是可以通过变量来访问属性。
var propertyName="name";
alert(person[propertyName]);
如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法。
person["first name"]="Nicholas";
通常,除非必须使用变量来访问属性,否则,建议使用点表示法。
13.操作方法
slice()
slice()能够基于当前数组中的一或多个项创建一个新数组。slice()方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下,slice()方法返回从该参数指定位置开始到当前数组的所有项。注意,slice()方法不会影响原始数组。
如果slice()方法的参数有一个负数,则用数组长度加上该数来确定相应的位置。例如,在一个包含5项的数组上调用slice(-2,-1)与调用slice(3,4)得到的结果相同。
splice()
splice()的主要用途是向数组的中部插入项,使用这种方法的方式有如下3种:
(1)删除:
可以删除任意数量的项,只需指定2个参数:要删除的第一项的位置和要删除的项数。例如,splice(0,2)会删除数组的前两项;
(2)插入:
可以向指定位置插入任意数量的项,只需提供3个参数:起始位置、0(要删除的项数)和要插入的项。如果要插入多个项,可以传入第四、第五,以至任意多个项。例如,splice(2,0,“red”,“green”)会从当前数组的位置2开始插入字符串"red"和"green"。
(3)替换:
可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定3个参数:起始位置、要删除的项和要插入的任意数量的项,插入的项数不必与要删除的项数相等。例如,splice(2,1,“red”,“green”)会删除当前数组位置2的项,然后再从位置2开始插入字符串"red"和"green"。
reduce()
使用reduce()方法可以执行求数组中所有项之和的操作。
var values=[1,2,3,4,5];
var sum=values.reduce(function(prev,cur,index,array){
return prev+cur;
});
alert(sum);//15
14.函数的内部属性
在函数内部,有两个特殊的对象:arguments和this。arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性,该属性时一个指针,指向拥有这个arguments对象的函数。
经典阶乘函数:
function factorial(num){
if(num<=1){
return 1;
}else{
return num*factorial(num-1);
}
}
定义阶乘函数一般都要用到递归算法,如上面的代码所示,有函数有名字,而且名字以后都不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名factorial紧紧耦合在一起,为了消除这种紧密耦合的现象,可以使用arguments.callee。
function factorial(num){
if(num<=1){
return 1;
}else{
return num*arguments.callee(num-1);
}
}
ES5也规范了另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。
function outer(){
inner();
}
function inner(){
alert(inner.caller);
}
outer():
以上代码会导致警告框中显示outer()函数的源代码。因为outer()调用了inner(),所以inner.caller就指向outer()。为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息。
function outer(){
inner();
}
function inner(){
alert(arguments.callee.caller);
}
outer();
15.字符串操作方法
ES提供了三个基于字符串创建新字符串的方法:slice()、substr()、substring()。第一个参数指定字符串的开始位置,第二个参数(在指定的情况下)表示字符串到哪里结束。具体来说,slice()和substring()的第二个参数指定的是子字符串最后一个字符后面的位置,而substr()的第二个参数指定的则是返回的字符个数。如果没有给这些方法传递第二个参数,则将字符串的末尾作为结束位置。
var stringValue="hello world";
alert(stringValue.slice(3));//"lo world"
alert(stringValue.substring(3));//"lo world"
alert(stringValue.substr(3));//"lo world"
alert(stringValue.slice(3,7));//"lo w"
alert(stringValue.substring(3,7));//"lo w"
alert(stringValue.substr(3,7));//"lo worl"
在传递给这些方法的参数是负值的情况下,它们的行为就不尽相同了。其中,slice()方法会将传入的负值与字符串的长度相加,substr()方法将负的第一个参数加上字符串的长度,而将负的第二个参数转换为0.最后,substring()方法会把所有负数参数都转换为0。
var stringValue="hello world";
alert(stringValue.slice(-3));//"rld"
alert(stringValue.substring(-3));//"hello world"
alert(stringValue.substr(-3));//"rld"
alert(stringValue.slice(3,-4));//"lo w"
alert(stringValue.substring(3,-4));//"hel"
alert(stringValue.substr(3,-4));""(空字符串)
16.原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。
使用hasOwnPrototype()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true。
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();
alert(person1.hasOwnPrototype("name"));//false
person1.name="Greg";
alert(person1.name);//"Greg"----来自实例
alert(person1.hasOwnPrototype("name"));//true
alert(person2.name);//"Nicholas"----来自原型
alert(person2.hasOwnPrototype("name"));//false
delete person1.name;
alert(person1.name);//"Nicholas"----来自原型
alert(person1.hasOwnPrototype("name"));//false
由于in操作符只要通过对象能够访问到属性就返回true,hasOwnPrototype()只在属性存在于实例中时才会返回true,因此只要in操作符返回true而hasOwnPrototype()返回false就可以确定属性是原型中的属性。
原型对象的问题:
原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性时被很多实例共享的,这种共享对于函数非常合适,对于那些包含基本值的属性倒也说得过去。毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。
function Person(){
}
Person.prototype={
constructor:Person,
name:"Nicholas",
age:29,
job:"SOftWare Engineer",
friends:["Shelby","Court"],
sayName:function(){
alert(this.name);
}
};
var person1=new Person();
var person2=new Person();
person1.friends.push("Van");
alert(person1.friends);//"Shelby,Court,Van"
alert(person2.friends);//"Shelby,Court,Van"
alert(person1.friends===person2.friends);//true
在此,Person.prototype对象有一个名为friends的属性,该属性包含一个字符串数组。然后创建了Person的两个实例。接着,修改了person1.friends引用的数组,向数组中添加了一个字符串。由于friends数组存在于Person.prototype而非person1中,所以刚刚提到的修改也会通过person2.friends(与person1.friends指向同一个数组)反映出来。实例一般都是要有属于自己的属性的,而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
组合使用构造函数模式和原型模式:
创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。另外,这种组成模式还支持向构造函数传递参数。
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.friends=["Shelby","Court"];
}
Person.prototype={
constructor:Person,
sayName:function(){
alert(this.name);
}
}
var person1=new Person("Nicholas",29,"SoftWare Engineer");
var person2=new Person("Greg",27,"Doctor");
person1.friends.push("Van");
alert(person1.friends);//"Shelby,Court,Van"
alert(person2.friends);//"Shelby,Court"
alert(person1.friends===person2.friends);//false
alert(person1.sayName===person2.sayName);//true
17.原型链
ES中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如让一个原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
实现原型链有一种基本模式,其代码大致如下:
function SuperType(){
this.prototype=true;
}
SuperType.prototype.getSuperValue=function(){
return this.property;
};
function SubType(){
this.subproperty=false;
}
// 继承了SuperType
SubType.prototype=new SuperType();
SubType.protptype.getSubValue=function(){
return this.subproperty;
};
var instance=new SubType();
alert(instance.getSuperValue());//true
18.闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
闭包与变量:
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。
function createFunction(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}
这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回10.因为每个函数的作用域链中都保存着createFunction()函数的活动对象,所以它们引用的都是同一个变量i。当createFunction()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10.但是我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示:
function createFunction(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=function(num){
return function(){
return num;
};
}(i);
}
return result;
}
在重写了前面的createFunction()函数后,每个函数就会返回各自不同的索引值了。在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数num,也就是最终函数要返回的值。在调用每个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,所以就会将变量i的当前值赋值给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样一来,result数组中的每个函数都有自己的num变量的一个副本,因此就可以返回各自不同的数值了。
内存泄漏:
由于IE9之前的版本对JS对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁。
function assignHandler(){
var element=document.getElementById("someElement");
element.onclick=function(){
alert(element.id);
};
}
以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此,它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示:
function assignHandler(){
var element=document.getElementById("someElement");
var id=element.id;
element.onclick=function(){
alert(id);
};
element=null;
}
在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。这样就能够解除对DOM对象的引用,顺利的减少其引用数,确保正常回收其占用的内存。
19.私有变量
严格来讲,JS中没有私有成员的概念,所有对象属性都是公有的,不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
如果在函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下:
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;
};
}
var person=new Person("Nicholas");
alert(person.getName());//"Nicholas"
person.setName("Greg");
alert(person.getName());//"Greg"
以上代码的构造函数中定义了两个特权方法:getName()和setName()。这两个方法都可以在构造函数外部使用,而且都有权访问私有变量name。但在Person构造函数外部,没有任何办法访问name。由于这两个方法是在构造函数内部定义的,它们作为闭包能够通过作用域链访问name。私有变量name在Person的每个实例中都不相同,因为每次调用的构造函数都会重新创建这两个方法。不过在构造函数中定义特权方法也有一个缺点,那就是必须使用构造函数模式来达到这个目的。构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法可以避免这个问题。
静态私有变量:
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下所示:
(function(){
// 私有变量和私有函数
var privateVariable=10;
function privateFunction(){
return false;
}
// 构造函数
MyObject=function(){
};
// 公有/特权方法
MyObject.prototype.publicMethod=function(){
privateVariable++;
return privateFunction();
};
})();
这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了所有变量和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的,出于同样的原因,也没有在声明MyObject时使用var关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法作为一个闭包,总是保存着对包含作用域的引用。
(function(){
var name="";
Person=function(value){
name=value;
};
Person.prototype.getName=function(){
return name;
};
Person.prototype.setName=function(value){
name=value;
};
})();
var person1=new Person(""Nicholas);
alert(person1.getName());//"Nicholas"
person1.setName("Greg");
alert(person1.getName());//"Greg"
var person2=new Person("Nicholas");
alert(person1.getName());//"Michael"
alert(person2.getName());//""Michael
这个例子中的Person构造函数与getName()和setName()方法一样,都有权访问私有变量name。在这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用setName()会影响所有实例。而调用setName()或新建一个Person实例都会赋予name属性的一个新值,结果就是所有实例都会返回相同的值。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量还是静态私有变量,最终要视具体需求而定。
20.window对象
抛开全局变量会成为window变量的属性不谈,定义全局变量与在window对象上直接定义属性还是有一点差别:全局变量不能通过delete操作符删除,而直接在window对象上定义的属性可以。
例如:
var age=19;
window.color="red";
// 在IE<9时抛出错误,在其他所有浏览器都会返回false
delete window.age;
// 在IE<9时抛出错误,在其他所有浏览器都会返回false
delete window.color;//return true
alert(window.age);//19
alert(window.color);//undefined