您的位置:首页 > Web前端 > JavaScript

javascript的相关知识

2011-11-23 11:26 239 查看

1.Javascript 垃圾收集机制

经常使用 Javascript 的人会琢磨其垃圾收集机制,Javascript 并不像 C,C++ 那样需要开发者手动去清除垃圾,在编写 Javascript 程序是,开发者无需关心内存使用问题,所需内存分配以及无用内存(垃圾)的回收完全实现了自动管理。究其根源,主要是程序收集那些不再使用的变量,并且释放其占用的内存。因此,垃圾收集机制会按照固定时间间隔,周期性反复的执行这一操作。

举例来说,局部变量只存在于函数内部,程序会为局部变量在栈内存或堆内存中分配对应的存储空间,当函数运行结束,局部变量所占用的内存就没有存在的必要了,这时程序会释放局部变量所占用的内存供其他变量使用。这是程序最简单释放内存的方法,但是很多时候,程序中变量会一直被使用,此时垃圾收集机制必须跟踪变量并且判断其是否被使用,是否可以释放其内存空间。

垃圾收集机制主要判断变量释放内存空间的方法有两个:其一是标记清除法,其二是引用计数法。

标记法,每个变量都有其运行环境,变量创建后会在某种环境中运行,比如创建一个局部变量,局部变量会运行在函数体内。当函数运行时,会标记局部变量为“进入环境”,当函数体运行结束后,意味着变量脱离了其运行环境,此时则将变量标记为“离开环境”。对于“离开环境”的变量,垃圾收集机制会进行相应记录,并且在下一个回收周期时将其释放。

引用计数法,跟踪记录每个值的被引用次数。声明一个变量并将一个引用类型值赋给该变量时,这个值得引用次数就是 1。如果同一个值又被赋给另外一个变量,则该值的引用次数加 1。相反,如果包含对这个值的引用的变量又取得另外一个值,这个值得引用次数减 1。当这个值得引用次数为 0 时,则说明没有办法再访问到此值,因此就可以将其占用的内存空间回收。当垃圾收集器在下一个周期运行时,会释放引用次数为零的值所占用的内存空间。(原文解释参考:Javascript 高级程序设计 - 第二版)

举个例子来说:
function countMethod(){
var object1 = new Object(); // 声明变量,计数器由 0 变为 1
var object2 = new Object(); // 声明变量,计数器由 0 变为 1
object1.method1 = object2; // object1 计数器 -1,object2 计数器 +1
object2.method2 = object1; // object1 计数器 +1,object2 计数器 -1
}
此函数运行退出后,object1 的计数器读数为 1,object2 的计数器度数为 1。所以两个变量都不会被销毁。如果大量的这样的程序存在于函数体内,就会导致大量的内存被浪费而无法回收,从而导致内存的泄露。

上述问题解决方法,手动释放 object1 object2 所占用的内存。即:
object1.method1 = null;
object2.method2 = null;

对比上面的例子,举一个正常情况下的例子。

function countMethod(){
var object1 = new Object(); // 声明变量,计数器由 0 变为 1
var object2 = new Object(); // 声明变量,计数器由 0 变为 1
object1.method1 = "This is object1"; // object1 计数器 -1,object1 读数变为0
object2.method2 = "This is object2"; // object2 计数器 -1,object2 读数变为0
}

通过上例看出,正常情况下,当函数运行结束后,object1 object2的读数均为 0,在下一个垃圾收集周期时,会被回收并且释放其所占用的内存。

2.javascript 原型函数 prototype 工作原理

Javascript 中的原型函数(prototype)的工作原理,在 javascript 中每次声明新函数的过程中,就会为其创建一个 prototype 的属性。在未加其他附带条件情况下,所有的 prototype 属性都会自动获取 constractor 属性,constructor 内包含一个指向 prototype 属性所属函数的指针(就是说 constructor 回指构造函数本身)。

举个例子来说,Fruit.prototype.constructor 指向 Fruit。并且可以通过这个构造函数,为其添加更多的属性和方法。

当调用构造函数创建一个新实例后,该实例内部包含一个指针指向构造函数的原型函数。此时我们不用去关心内部的这个指针到底是什么(这个指针还的确有个名字:__proto__ 估计是为了对应 prototype 而起的名字吧 ~\(≧▽≦)/~ ),只需记住它的指向即可(指向构造函数的原型函数)。需要注意的是,这个 __proto__ 只存在于函数实例与构造函数的原型函数之间,而非实例与构造函数之间。

下面画个图,来精心诠释一下。



如上图所示,Fruit_1, Fruit_2 与构造函数没有直接的联系,只是这两个实例的 __proto__ 属性指向了 Fruit.prototype。虽然这两个实例都不包含属性和方法,但却可以通过 fruit_1.showPrice() 来调用。其理论依据是通过查找对象属性的过程来实现的。

举个例子来说:

Javascript代码

function Fruit(){
}

Fruit.prototype.category = "apple";
Fruit.prototype.price = 19.9;
Fruit.prototype.showPrice = function(){
alert(this.price);
}

var fruit_1 = new Fruit();
var fruit_2 = new Fruit();
alert(fruit_1.constructor == fruit_2.constructor);  // 输出 true
fruit_1.showPrice(); // 19.9


此时,Fruit()构造函数变成了一个空函数,但却可以通过调用 prototype 往构造函数内直接增加属性和方法。并且在此时,仍然可以调用构造函数来 new 新对象,并且新对象具有通过原型函数向构造函数直接增加的属性和方法(有点拗口啊,就是说,通过原型函数直接向构造函数增加属性和方法,增加的这些属性和方法,在通过构造函数 new 出来新实例中也具有)。

并且通过上面的例子可以看出,通过构造函数 new 出来的不同对象,具有与构造函数相同的属性和方法。并且这些都是共有的。

这一切的一切表明,在构造函数外部可以通过原型函数为其增加属性和方法,并且与在构造函数内部声明具有相同的效果。

3.Javascript 没有重载的理解

在 Javascript 中,是没有重载的概念的。我们可以通过将函数名想像为指针的方法对其加以深入的理解(很好理解)。

下面的一个例子可以让大家很容易的明白 Javascript 中无重载的概念。

Js代码
function addNum(s1){
return 100+s1;
}

function addNum(s2){
return 200+s2;
}

alert(addNum(1)); // 输出 201,实际上第二个函数覆盖了第一个函数。


看下面的变量声明函数法对上述概念的理解:

Js代码
var addNum = function(s1){
return 100+s1;
};

addNum = function(s1){
return 200+s1;
};

alert(addNum(1)); // 输出 201,实际上第二个函数覆盖了第一个函数。


对于变量的声明,实际上此处的 addNum 是对 Function 对象的一个引用,可以想象成指针,变量声明了两次,实际上第二次的声明切断了函数第一次的引用而指向了另外一个对象。

在这里,我们可以把每个函数都看做是 Function 类型的实例。由于这里的函数是对象,则函数名实际上是指向函数对象的指针,而并不会与某个具体的函数绑定。

4.由 OO 继承来谈谈 javascript 继承

在大多数面向对象语言中,基本上的都支持继承,首先来宽泛的谈谈大多数 OO 语言的继承方式,之后具体到 javascript 来看看其继承有什么不同之处。

1. 实现继承:实现继承是指派生类(子类)继承了基类(父类)的所有属性和方法,并且有且只有一个基类。

优点是可以直接使用基类的所有属性和方法,缺点不言而喻,基类的一些不必要的方法也会被子类所继承。

比如:基类定义了果树类,里面有开花,结果等方法。派生类继承基类,但如果派生类的中的果树不会开花,只会结果(如:无花果),那么开花对子类就没用,但子类确实继承了基类开花的方法。

在设计模式中,我们更多强调的是面向接口的继承。上面的例子中,果树有两个接口,一个是开花,一个是结果。如果我的果树只能结果,不会开花的话,那么只要我的果树实现结果的接口就行了。与此同时不会把开花带入到我的派生类(子类)中。

2. 接口继承:派生类继承了接口的方法签名,它不同于实现继承的是,接口继承允许多继承,同时派生类只继承了方法签名而没有方法实现。具体的实现必须在派生类中完成。这种继承又称为“接口实现”。

谈完了 OO 语言继承的分类,下面对比上述两种方式来看看 javascript 是怎么来完成它独有的继承的。

先看第二点——接口继承,接口继承要求派生类继承基类的方法签名。

方法签名:返回值类型+方法名+参数列表

而在 javascript 中,任何的函数,方法,究其本质都会转变成变量来解析,如下:

Javascript代码
//定义式
function a(){
alert("pluto");
}

//变量式
var a = function(){
alert("Pluto");
}


两种声明方式除了写法不同,执行顺序不同(定义式在编译时会自动提前)之外,其余都相同。并且由第二种方法可以看出,函数其实是一个命了名的变量而已。

由于函数没有签名,所以接口继承的方式在 javascript 中就不复存在了。

下面着重来谈谈第一点——实现继承。

实现继承主要是依靠 javascript 中的原型链来实现,并且将原型链作为实现继承的主要方法。

回顾一下构造函数,原型,实例之间的关系:构造函数都有一个原型对象(prototype),原型对象(prototype)都有一个回指构造函数的指针(constructor),而实例包含一个指向原型对象的指针(__proto__)。如果将“Fruit”的实例赋值给另外一个构造函数 “NotFruit” 的原型对象:即:

Javascript代码
function Fruit(){
this.fruit = true;
}

Fruit.prototype.isFruit = function(){
return this.fruit;
}

function NotFruit(){
this.notFruit = false;
}

NotFruit.prototype = new Fruit();//将  Fruit()的实例赋值给另外一个构造函数 (NotFruit) 的原型对象 (NotFruit.prototype)

NotFruit.prototype.isNotFruit = function(){
return this.notFruit;
}

var dog = new NotFruit(); // 创建派生类(子类)的实例 dog

alert(dog.isFruit());    // 输出 true
alert(dog.isNotFruit()); // 输出 false
alert(dog.notFoundFruit()); // 报错  dog.notFoundFruit is not a function


则此时的原型对象包含了一个指向另外一个原型对象的指针。如下图诠释:



如上所示,便完成了 javascript 通过原型对象的方式的继承。

原型链表明实例化的对象首先会在构造函数的实例中搜索该属性,如果没有找到,则会继续搜索实例的原型。通过原型链实现继承,搜索过程会沿着原型链向上搜索,拿上面的例子来说,调用 dog.isFruit() 会经历三个搜索步骤:1)搜索实例(未找到) --> 2)搜索 NotFruit.prototype (未找到) --> 3) 搜索 Fruit.prototype,上面这个例子在调用 dog.isFruit() 时,在第三步才找到其对应的方法。如果在搜索到原型末端还未找到属性或方法时,则会报错,如上 dog.notFoundFruit()
所示。

当然,在 javascript 中,还存在“对象冒充”的方式的继承,在这里就不详细论述了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: