一个完整的JavaScript实现应由以下三个部分组成:
ECMASript (核心)
DOM (文档对象模型)
BOM (浏览器对象模型)
数据类型
ECMAScript中有五种简单数据类型:
Underfined
Null
Boolean
Number
String
一种复杂数据类型:
Object
注意!
console.log(typeof(null)); //object
小心with
with语句会降低性能。
与其他语言函数的一些不同之处
ECMAScript函数命名的参数只提供便利,但不是必须的,不介意传递进去多少个参数,也不在乎传进来参数是什么数据类型。
未指定返回值的函数返回的是一个特殊的undefined值。
ECMAScript中所有的参数传递的都是值,不可能通过引用传递参数。
ECMAScript函数没有签名,因为其参数是由包含零或多个值的数据来表示。因为没有签名,所以没有重载。
JavaScript解释器在执行环境中加载数据时,会率先读取函数声明,使其在执行任何代码之前可用;至于函数表达式,则必须等到解释器执行到它所在的代码行,才会被真正解释执行。
alert(sum(10,10));
function sum(num1,num2){
return num1 + num2;
}
如果将函数声明改为等价的函数表达式,就会在执行期间导致错误。
alert(sum(10,10));
var sum = function(num1,num2){
return num1 + num2;
}
函数嵌套:
实现排序
function createComparisonFunction(propertyName) {
return function(object1,object2){
var value1 = object1[propertyName];
var value2 = object2propertyName];
if(value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
var data = {{name:"test",age:28},{name:"hello",age:"25"}};
data.sort(createComparsionFunction("name"));
data.sort(createComparsionFunction("age"));
函数内部有两个特殊的对象: arguments和this,arguments有一个callee的属性,是一个指针指向拥有arguments对象的函数。
//实现递归
function factorial(num) {
if(num<=1){
return 1;
} else {
return num*arguments.callee(num - 1);
}
}
this引用的是函数据以执行的环境对象。当在网页的全局作用域中调用函数时,this引用的是windows.
window.color = "red";
var o = { color: "blue"};
function sayColor(){
alert(this.color);
}
sayColor(); //"red"
o.sayColor = sayColor;
o.sayColor(); //"blue"
ECMAScript中的函数是对象,每个函数都有两个属性:length和prototype.
length表示函数希望接受的命名参数的个数。
call()与apply()
prototype是保存它们所有实例方法的真正所在,诸如toString()和valueFO()等方法都保存在prototype下。
每个函数都包含两个非继承来的方法:apply()和call(),两个方法都在特定的作用于中调用函数,实际上等于设置函数体内this对象的值。apply()方法接受两个参数:一个在其中运行函数的作用于,另一个是参数数组。
function sum(num1,num2){
return num1 + num2;
}
function callSum1(num1,num2){
return sum.apply(this,arguments);
}
function callSum2(num1,[num1,num2]);
}
alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20
call()方法与apply()作用相同,区别在于接收参数的方式不同。对于call()方法而言,第一个参数是this值,变化的是其与参数都直接传递给函数。
传递参数并非apply()和call()真正用武之地:它们真正强大的是能扩大函数的作用域。
window.color = "red";
var o = {color:"blue"};
function sayColor(){
alert(this.color);
}
sayColor(); //red
sayColor.call(this); //red
sayColor.call(window); //red
sayColor.call(o); //red
bind()
bind()
方法创建一个函数的实例,其this值会被绑定到传递给bind()函数的值。
window.color = "red";
var o = {color:"blue"};
function sayColor(){
alert(this.color);
}
var objectSayColor = sayColor.bind(o) ;
objectSayColor(); //blue
引用类型与基本包装类型的区别
引用类型与基本包装类型的主要区别就是对象的生存期。使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。来看下面的例子:
var s1 = "some text";
s1.color = ""red;
alert(s1.color); //undefined
内置对象
ECMA-262 对内置对象的定义是: “由 ECMAScript 实现提供的、不依赖于宿主环境的对象,这些对象在 ECMAScript 程序执行之前就已经存在了。”意思就是说,开发人员不必显式地实例化内置对象,因为它们已经实例化了。
Global(全局)对象可以说是 ECMAScript 中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的。ECMAScript 中的 Global 对象在某种意义上是作为一个终极的“兜底儿对象来定义的。换句话说,不属于任何其他对象的属性和方法,最终都是它的属性和方法。事实上,没有全局变量或全局函数;所有在全局作用域中定义的属性和函数,都是 Global 对象的属性。
eval()
当解析器发现代码中调用 eval()方法时,它会将传入的参数当作实际的 ECMAScript 语句来解析,然后把执行结果插入到原位置。通过 eval()执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链。这意味着通过 eval()执行的代码可以引用在包含环境中定义的变量,举个例子:
//在环境外定义
var msg = "hello world!";
eval('alert(msg)'); //"hello world!"
//在eval环境内定义
eval("function sayHi() { alert('Hi');}");
sayHi();
window ECMAScript 虽然没有指出如何直接访问 Global 对象,但 Web 浏览器都是将这个全局对象作为window 对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了 window对象的属性。
操作基本类型值的语句一经执行完毕,就会立即销毁新创建的包装对象。在所有代码执行之前,作用域中就已经存在两个内置对象: Global 和 Math 。在大多数ECMAScript实现中都不能直接访问 Global 对象;不过,Web 浏览器实现了承担该角色的 window 对象。
面向对象程序设计
ECMAScript 中有两种属性:
-
数据属性
要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法。这个方法接收三个参数: 访问器属性
访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。
访问器属性不能直接定义,必须使用Object.defineProperty()
来定义:
Object.defineProperty(book,"year",{
get: function(){
return this._year;
},set: function(newValue){
if (newValue>2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); //2
ECMAScript 5 又定义了一个 Object.defineProperties() 方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。
创建对象
工厂模式
用函数来封装以特定接口创建对象的细节:
function createPerson(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 person1 = createPerson("Nicholas",29,"coder");
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模式
与工厂模式相比:
function Person(name,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = Person("Nicholas","coder");
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4
个步骤:
构造的新对象有一个 constructor (构造函数)属性:
alert(person1.constructor == Person); //true
检测对象类型,还是 instanceof 操作符要更可靠一些。
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
原型模式
我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那
么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person();
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "coder";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。
funciton Person(name,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Tom','jobs'];
}
Person.prototype = {
constructor: Person;
sayName: function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas","Software Engineer");
动态原型模式
动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name,job){
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this,name);
};
}
}
var person1 = Person("Nicholas","coder");
friend.sayName();
这里只在 sayName() 方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。
寄生构造函数模式
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
function Person(name,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","Software Engineer");
friend.sayName(); //"Nicholas"
继承(TODO)
由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
ECMAScript 支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。
函数表达式
匿名函数(anonymous function)
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
但实际上,每个函数都返回 10。因为每个函数的作用域链中都保存着 createFunctions() 函数的活动对象,所以它们引用的都是同一个变量 i 。当createFunctions() 函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是 10。
我们可以创建另一个匿名函数强制让闭包的行为符合预期:
function createFunctions(){
var result = new Array();
for (var i=0;i<10;++i){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数 num ,也就是最终的函数要返回的值。在调用每个匿名函数时,我
们传入了变量 i 。由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num 。而在这个匿名函数内部,又创建并返回了一个访问 num 的闭包。这样一来, result 数组中的每个函数都有自己num 变量的一个副本,因此就可以返回各自不同的数值了。
this对象
this 对象是在运行时基于函数的执行环境绑定的:在全局函数中, this 等于 window ,而当函数被作为某个对象的方法调用时, this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。当然,在通过 call() 或 apply() 改变函数执行环境的情况下, this 就会指向其他对象。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。
var name = "The Window";
var object = {
name : "My Object",getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"
将外部作用域中的this对象保存在一个闭包能访问到的对象:
var name = "The window";
var object = {
name : "My object",getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My object"
几种特殊情况下,this的值会被意外的改变:
var name = "The Windows";
var object = {
name : "My Object",getName: function(){
return this.name;
}
};
object.getName(); //"my object"
(object.getName)(); //"my object"
(object.getName = object.getName)(); //"The window"
第一行代码跟平常一样调用了 object.getName() ,返回的是 "My Object" ,因为 this.name就是 object.name 。第二行代码在调用这个方法前先给它加上了括号。虽然加上括号之后,就好像只是在引用一个函数,但 this 的值得到了维持,因为 object.getName 和 (object.getName) 的定义是相同的。第三行代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以 this 的值不能得到维持,结果就返回了 "The Window" 。
模仿块级作用域
JavaScript 没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。
JavaScript 从来不会告诉你是否多次声明了同一个变量;遇到重复声明,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。
(function(){
//块级作用域
})();
以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。
但是下面的代码会导致错误:
function(){
//块级作用域
}();
这段代码会导致语法错误,是因为 JavaScript 将 function 关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号。要将函数声明转换成函数表达式,只要这样给它加上一对圆括号即可。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域.
function outputNumbers(count){
(function (){
for (var i=0;i
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
私有变量
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。
function MyObject(){
var privateVariable = 10;
}
BOM
window对象
由于 window 对象同时扮演着 ECMAScript 中 Global 对象的角色,因此所有在全局作用域中声明的变量、函数都会变成 window 对象的属性和方法。
抛开全局变量会成为 window 对象的属性不谈,定义全局变量与在 window 对象上直接定义属性还是有一点差别:全局变量不能通过 delete 操作符删除,而直接在 window 对象上的定义的属性可以。
另外,还要记住一件事:尝试访问未声明的变量会抛出错误,但是通过查询 window 对象,可以知道某个可能未声明的变量是否存在。