重新认识javascript(五)

来源:互联网 发布:xp电脑网络起动慢 编辑:程序博客网 时间:2024/06/05 05:31


自定义对象

注意:想要了解更多JavaScript中面向对象编程的知识,参考Introduction to Object Oriented JavaScript.

在传统的面向对象编程中,对象是数据的集合,而方法就是操作这些数据。JavaScript是一个基于原型的语言,它没有C++Java中的类定义语句。这对习惯了类声明语句的开发者来说会有些困惑。JavaScript使用函数来作为类。现在我们构思一个person对象,里面有firstnamelastname属性。有两种显示名字的方式:如 first last last first。接下来是一种实现方式,使用我们之前讨论的函数和对象。

function makePerson(first, last) {    return {        first: first,        last: last    }}function personFullName(person) {    return person.first + ' ' + person.last;}function personFullNameReversed(person) {    return person.last + ', ' + person.first;}> s = makePerson("Simon", "Willison");> personFullName(s)Simon Willison> personFullNameReversed(s)Willison, Simon

像上面那样就可以实现,但是相当丑陋。在全局命名空间中使用了许多函数。我们真正需要的一种方式是添加一个函数到对象中,因为函数就是对象,所以要这么做很简单:

function makePerson(first, last) {    return {        first: first,        last: last,        fullName: function() {            return this.first + ' ' + this.last;        },        fullNameReversed: function() {            return this.last + ', ' + this.first;        }    }}> s = makePerson("Simon", "Willison")> s.fullName()Simon Willison> s.fullNameReversed()Willison, Simon

有一些之前我们未关注的事情:this关键字。在函数体内使用,this是指代当前对象。这实际上就说明你调用函数的方式。如果你在一个对象上使用点运算符或括号调用它,这个对象就是this。如果在调用时没有使用点运算符。This就指代的是全局对象。这个经常引起误解。例如

> s = makePerson("Simon", "Willison")> var fullName = s.fullName;> fullName()undefined undefined

当我们调用fullName(),this指的就是全局对象。由于没有全局对象叫firstlast,所以我们得到的值就都是undefined

我们可以利用this关键字来优化我们的makPerson函数

function Person(first, last) {    this.first = first;    this.last = last;    this.fullName = function() {        return this.first + ' ' + this.last;    }    this.fullNameReversed = function() {        return this.last + ', ' + this.first;    }}var s = new Person("Simon", "Willison");

接下来介绍另一个关键字,newnewthis有很强的联系。它做的事情就是创建一个全新的空对象,然后调用指定的方法,然后把this赋值给新对象。New调用的指定函数就叫做构造函数。通用的做法是把这类函数的首字母大写,用来提醒它们是被new调用的。

我们的person对象变得越来越好了,但是它们的边边角角依然有些丑陋。每次我们创建一个对象的时候,我们都会在它里面创建两个全新的函数对象如果这部分代码共享,它是不是会变得更好?

function personFullName() {    return this.first + ' ' + this.last;}function personFullNameReversed() {    return this.last + ', ' + this.first;}function Person(first, last) {    this.first = first;    this.last = last;    this.fullName = personFullName;    this.fullNameReversed = personFullNameReversed;}

这样就更好了:我们只创建一个方法函数,并且在构造函数里面引用这些函数。我们还能做得更好吗?答案是yes

function Person(first, last) {    this.first = first;    this.last = last;}Person.prototype.fullName = function() {    return this.first + ' ' + this.last;}Person.prototype.fullNameReversed = function() {    return this.last + ', ' + this.first;}

Person.prototype是一个被Person所有实例共享的对象。它组成了查找链的一部分(有一个专业的名称,原型链)。任何时候你想访问一个Person未设置的属性,JavaScript会检查Person.prototype来看这个属性是否存在。这样的结果就是,对于这个构造函数所有的实例,任何在Person.prototype中定义的东西,使用this对象都可以访问得到。

这是一个非常强大的工具。JavaScript允许你在程序中任意时候去修改某个对象的原型,意味着你可以在运行时添加附加的方法到已存在的对象中:

> s = new Person("Simon", "Willison");> s.firstNameCaps();TypeError on line 1: s.firstNameCaps is not a function> Person.prototype.firstNameCaps = function() {    return this.first.toUpperCase()}> s.firstNameCaps()SIMON

难以置信,你也可以在JavaScript内置对象的原型上添加东西。让我们在String中添加一个方法,返回字符串的逆序:

> var s = "Simon";> s.reversed()TypeError on line 1: s.reversed is not a function> String.prototype.reversed = function() {    var r = "";    for (var i = this.length - 1; i >= 0; i--) {        r += this[i];    }    return r;}> s.reversed()nomiS

这个方法在字符串常量上面也同样生效

> "This can now be reversed".reversed()desrever eb won nac sihT

就像之前提到的,原型会形成一个链表。这个链表的根式Object.prototype,它里面包含有toString方法当你想把一个对象当做字符串使用的时候,这个方法就会被调用到。这个方法对我们调试Person对象是很有用的。

> var s = new Person("Simon", "Willison");> s[object Object]> Person.prototype.toString = function() {    return '<Person: ' + this.fullName() + '>';}> s<Person: Simon Willison>

记得avg.apply的第一个参数是null么,我们现在可以再访问它。Applay的第一个参数是一个对象,这个对象应该被当做this。例如,这里有一个new的简单实现。

function trivialNew(constructor) {    var o = {}; // Create an object    constructor.apply(o, arguments);    return o;}

这不是new的完美实现,因为它没有设置到apply的原型链上(这个很难举例说明),这个东西不会常常使用,但是有必要知道这个。

Apply有一个姊妹函数叫做callcall也允许你设置this,与apply传一个数组不同,它的入参是一个扩展的参数列表。

function lastNameCaps() {    return this.last.toUpperCase();}var s = new Person("Simon", "Willison");lastNameCaps.call(s);// Is the same as:s.lastNameCaps = lastNameCaps;s.lastNameCaps();

内部函数

JavaScript允许函数声明在其他函数中。我们在之前见到过一次,在一个比较早的makePersion的函数中。JavaScript函数的一个重要的细节是它们可以访问父函数作用域中的变量。

function betterExampleNeeded() {    var a = 1;    function oneMoreThanA() {        return a + 1;    }    return oneMoreThanA();}

这为写可维护性代码提供了许多便利。如果一个函数依赖一个或两个函数,这一个或两个被依赖的函数在你代码中其他地方根本就没用。你可以把这些实用的函数嵌套在函数中。这样可以控制全局域中的函数数目,这么做很有意义。

这也是一个不错的应对全局变量诱惑的方式。当书写复杂代码的时候,在多个函数中使用全局变量来共享值是很常见的---这会导致代码很难维护。嵌套函数可以共享父函数中的变量,因此在适当场景下你可以使用这个机制(“本地全局”变量)把函数结合在一起,而不用担心会污染全局命名空间。这个技术应该慎重使用,但它确实是一个有用的技巧。


闭包

闭包引导我们接触到JavaScript提供的最有用的抽象体之一,但也是最有可能令人困惑的。它到是做什么的呢?

function makeAdder(a) {    return function(b) {        return a + b;    }}x = makeAdder(5);y = makeAdder(20);x(6)?y(7)?

名字是MakeAdder的函数应该被放弃:它创建了新的adder函数,这些新的函数是在makeAdder接收一个入参被调用的时候创建。

这里出现的和之前看到的内部函数中看到的很是相像:一个函数定义在另一个函数里面,然后可以访问外部函数的变量。唯一的区别是闭包中外部函数已经返回了,因此常识可能告诉我们它的本地变量不再存在了。但它们确实是存在的-否则这个adder函数是不能够正常工作的。更重要的是,有两份不同makeAdder本地变量的拷贝,一个里面的a5,另一个里面的a20.所以这些函数调用后的结果如下:

x(6) // returns 11y(7) // returns 27
这就是真实发生的。任何时候JavaScript执行一个函数,一个作用域对象会被创建,用来保存在函数内创建本地变量。它和其他作为函数参数传入的变量一起被初始化。这与所有全局变量和函数所在的全局对象一样,但还是有一些重要的差异:首先,每次一个函数开始执行的时候,一个全新的作用域对象就会被创建,其次,和全局对象(就像浏览器中的window对象一样)不同的是,这些作用域对象在你的JavaScript代码是不能直接访问到的。举个例子,没有途径可以遍历当前作用域对象的属性。

因此当makeAdder被调用时,一个作用域对象和属性a一起被创建,a就是作为参数传递给makeAdder函数的。接着makeAdder返回一个新创建的函数。正常情况下,JavaScript的垃圾回收器会在这个时候清除掉makeAdder创建的作用域对象,但是里面返回的函数保存了一个这个作用域对象的引用。结果,这个域作用域对象就不会被当做垃圾回收掉,直到makeAdder返回的函数对象不再引用这个作用域对象为止。

作用域对象形成一个链,叫做作用域链,和JavaScript的对象系统中的原型链类似。

一个闭包是函数和它里面创建的作用域对象的组合。

闭包让你能够保存状态,正因如此,它们常备用来代替对象。


内存泄露

闭包有一个令人感到遗憾的副作用,在IE下它非常容易导致内存泄露。JavaScript是一个垃圾回收的语言---对象在创建的时候分配内存,然后在没有引用只想这个对象的时候浏览器会回收这部分内存。宿主对象提供的对象是由当前环境来处理的。

宿主浏览器需要管理大量DOM对象,这些DOM对象是由HTML页面渲染的。它们是由浏览器来管理分配和回收。

IE使用它自带的垃圾回收模式,独立于JavaScript使用的机制。由于它们之间的交互从而导致了内存泄露。

IE下,只要一个JavaScript对象和本地对象之间形成了一个循环引用,就会导致内存泄露。看如下示例:
function leakMemory() {    var el = document.getElementById('el');    var o = { 'el': el };    el.o = o;}

上面产生的循环引用导致了内存泄露,IE下不会释放elo使用的内存,除非浏览器重启。

上面例子中泄露的内存很容易被忽略;内存泄露在下面场景中才会变成现实:当存在一个长时间运行的应用或由于大型的数据结构或循环中泄露模式从而导致泄露了大量的内存。

泄露很少这么明显---通常泄露的数据结构有许多层引用,很难看出是循环引用。

不用刻意去做,闭包中很容易就能构造出一个内存泄露,看下面例子:

function addHandler() {    var el = document.getElementById('el');    el.onclick = function() {        this.style.backgroundColor = 'red';    }}

上面的代码中,当元素被点击的时候,将元素的背景色置为红色。它也会导致内存泄露,为什么?因为el的引用无意中被放到匿名的内部函数中。这样就在JavaScript对象(这个函数)和本地对象(el)之前形成了一个循环引用。

针对这个问题有许多解决的办法。最简单的就是不使用el变量:

function addHandler(){    document.getElementById('el').onclick = function(){        this.style.backgroundColor = 'red';    }}

令人惊讶,一个破坏由闭包引入的循环引用的窍门是增加另一个闭包:

function addHandler() {    var clickHandler = function() {        this.style.backgroundColor = 'red';    };    (function() {        var el = document.getElementById('el');        el.onclick = clickHandler;    })();}
内部函数会立马执行,并把它的内容隐藏起来,不让clickHander创建的闭包发现。

另一个阻止闭包的不错的方法是在window.onunload事件中破坏循环引用。许多事件库会替你做这些事情。注意在fireforx1.5中这么做会禁用bfcache,因此在firefox中你不应该注册一个unload监听器,除非你有其他原因。


原文信息:

·  Author: Simon Willison

·  Last Updated Date: March 7, 2006

·   Copyright: © 2006 Simon Willison, contributed under the Creative Commons:Attribute-Sharealike 2.0 license.

·  More information: For more information about this tutorial (and for linksto the original talk's slides), see Simon's Etech weblog post.