JavaScript,你懂的

标签: javascript 你懂的 | 发表时间:2011-04-07 11:41 | 作者:keakon dylan
出处:http://www.keakon.net/
经常有人问我,JavaScript应该怎么学。
这个问题其实很好回答:
  1. 先学基本语法,如果曾学过C等语言,应该1小时内就能掌握了。
  2. 再去使用内置的函数、方法和DOM API,熟悉它能干什么;而在学习DOM API的过程中,你还不得不与HTML和CSS打交道。
  3. 然后弄懂匿名函数和闭包,学会至少一个常用的JavaScript库(例如jQuery)。
  4. 最后领悟它的对象实现,尝试去扩展已有库,或编写自己的库。

可学习过程却并不像回答般轻松,因为国内的网站到处充斥着转载。那些人连自己都没弄懂的玩意就转载出来,甚至包含一些明显的错误和不堪入目的代码,却还有一大批小白惊呼“好强大,看不懂”。
在这种可悲的环境中,想要学好JavaScript还真棘手,因为你得有足够的经验来判断这篇文章是否值得一看,内容是否有错,哪些地方可以改进。
为了避免这种弯路,我还是自己写篇来整理整理吧。前2个阶段的就不提了,那是浪费你我的时间。


匿名函数

JavaScript中的函数定义很简单:
function 函数名(参数) {
// 函数体
}
当不写函数名的时候,它就是一个匿名函数了,只不过你没法通过函数名来引用它。

很显然,匿名函数也是函数,而函数是Function的实例(对象),它还可以用这种方式来创建:
new Function('参数名', '/* 函数体 */');

由于匿名函数是个对象,因此可以把匿名函数对象赋值给一个变量:
var 函数名 = function(参数) {
// 函数体
}
第1和第3段代码实现的效果是完全相同的,就是定义一个函数,并与“函数名”这个名称进行绑定。

当然,就算不与某个名称绑定,我们照样可以调用一个匿名函数:
(function(a, b) {
	return a + b;
})(1, 2);
第一个括号内是匿名函数的定义,这个括号的作用是提高运算优先级,以便引用这个匿名函数对象;最后那个括号内则是调用它的实际参数,因此这段代码的作用就是将1和2作为参数传递给一个匿名函数,并返回结果3。
这样做有什么好处呢?很简单,在这个匿名函数里定义的变量名只在其内部有效,不会影响全局名字空间,不用担心命名冲突。

此外,我们还能利用arguments.callee来实现递归调用匿名函数,这就是一个计算斐波那契数列的实现:
(function (n) {
	if (n <= 2) {
		return 1;
	}
	return arguments.callee(n - 1) + arguments.callee(n - 2)
})(10);

和其他对象一样,函数可以作为参数传递给另一个函数,匿名函数也不例外:
function 高阶函数(低阶函数, 参数) {
	return 低阶函数(参数) * 2;
}

高阶函数(function(x) {
	return x + 1;
}, 3);
这段代码中,高阶函数的函数体里调用了低阶函数,就像C中的函数指针一样,所以应该不难理解。
重点是在调用高阶函数时,直接传递了一个匿名函数作为参数(不需要括号)。它实际上相当于:
var 匿名函数 = function(x) {
	return x + 1;
}

高阶函数(匿名函数, 3);
所不同的是“匿名函数”这个变量名并不存在,而是在调用时直接作为一个函数对象传递给了高阶函数。


闭包

之前我也曾写过一篇《JavaScript的闭包》,但在这篇文章里,我想用更多的代码来解释。

正如前面所说,函数也是对象,那么在函数体内部定义一个函数对象是很合理的:
function f() {
	function g() {
	}
}

更进一步地,内部函数可以使用外部作用域的变量:
var a = 1;
var b = 2;

function f() {
	var a = 3;
	function g(c) {
		var d = 4;
		/*
		g里定义了d,因此d为4
		g的参数里有c,因此c为f传递给它的5
		g里没有定义a,而外层的f定义了a,因此a为3
		g和f里都没定义b,而全局名字空间里有b,因此b为2
		顺带一提,如果连全局名字空间里都没有的话,那就是undefined了
		*/
		return a + b + c + d;
	}
	return g(5);
}

函数可以返回内部的变量给外部作用域,而函数对象本身也是个变量,因此也可以作为返回值:
function f() {
	function g(x) {
		return x + 1;
	}
	return g;
}

f()(2);
其中f()的值就是f函数内部的g函数,因此相当于调用的是g(2)。

更进一步地,这个内部函数也可以使用外部作用域的变量:
var a = 1;

function f(b) {
	var c = 3;
	function g() {
		var d = 4;
		return a + b + c + d;
	}
	c = 5;
	return g;
}

var h = f(2);
h(); // 12

a = 6;
h(); // 17

f(7)(); // 22
这段代码很奇特,外部的h被赋值为f(2),因此f和g中的b都是2。那么c是多少呢?在定义g之前,它是3,可在定义之后又被重新赋值为5了。
实际上根本无需纠结,函数在定义时是不会去执行内部的代码的,只有在调用时才会代入这些变量的值。而在调用时,c已经变成了5,所以就以5计算了。同样的,更改a和b也会影响最终的结果。
而且,你甚至可以把g函数的定义提到变量c的声明之前,结果仍然是不变的,并不会出现c不存在的错误。(事实上很多JavaScript编译器就是这么做的。)

如果你看懂了上述这些代码,那么你其实已经弄懂闭包了。
实际上,当你在内部函数g里使用外部函数f的变量c时,为了能在f执行结束(return)后仍能引用它里面的c,c就不能被销毁,而是与g绑定在一起了。此处的变量c就被称为自由变量,而绑定了自由变量的函数g就被称为闭包。
另外,为了说明的方便,我将这个内部函数命名为g了,其实它也可以是匿名函数。

那么闭包有什么用呢?
举个很简单的例子:假如我的函数要用到一个值,这个值的计算很耗时间,但每次计算的结果实际上都一样。那么正常人肯定会先用一个变量保存这个计算结果,然后才去调用这个函数。
但是这个值我并不希望暴露到全局名字空间,那么就必须作为内部变量了。而让闭包去引用这个内部变量,一切就迎刃而解了。

注意,我不止一次地提到尽量不要将变量名暴露到全局名字空间,这样做的好处你慢慢会懂的。


对象

在JavaScript中,并非一切都是对象,例如null和undefined。此外还有1、0.0、NaN、true和"hello"等简单类型的对象,它们虽然也有方法和属性,但并不在我所要讨论的范畴中。
剩下的几乎全是对象,例如{}、[]、/1/、new Date()和function(){}等。这些对象的共同点是typeof的值是"object"或"function",且instanceof Object的值为true。(注意typeof(null)的值是"object",但null并没有方法和属性,因此我不认为它是对象。)
由此可见,JavaScript的对象都是Object的实例。

那么Object、对象和函数究竟是什么?
在解释之前前,我还是先上一段代码:
>>> typeof(Object)
"function"
>>> typeof((function(){}))
"function"
>>> (function(){}) instanceof Object
true
>>> (function(){}) instanceof Function
true
>>> typeof(new Object())
"object"
>>> (new Object()) instanceof Object
true
>>> (new Object()) instanceof Function
false
>>> Object instanceof Object
true
>>> Function instanceof Function
true
>>> Function instanceof Object
true
>>> Object instanceof Function
true
由于typeof(Object)的值是"function",并且Object还是Function的实例,因此它必然是个函数。
而所有的对象都是Object或其子类的实例,由此可得,所有的对象都是函数的实例,并且它们的类都是函数。这听上去很奇怪,但这也正是JavaScript与其他面向对象语言的不同点之一。

在大部分面向对象的语言中,类是对象的模板,一个类的所有实例都共享这个类的方法和属性;而与此同时,类还提供了构造器来初始化实例。
而在JavaScript中则正好相反:函数本身用来充当构造器,而与此同时,函数的原型(prototype)则被函数的实例所共享。
也就是说,当函数作为对象模板来使用时,它的实例是对象,而不要想当然地认为函数的实例就是函数(例如new Object()就不是)。

函数作为一种对象,它必然也是某个函数的实例,这个函数就是Function。
由于Function本身就是函数,所以它也是Function的实例。
再由于函数都是对象,Function也不例外,所以Function也是对象,因此它也是Object的实例。
而Object是函数,因此也是Function的实例。
此外Object还是对象,所以Object也是Object的实例。
这就是函数和对象之间乱七八糟的关系,看不懂也没关系,认真你就输了~

在你彻底凌乱之前,我先总结一下吧:
  • 对象是函数的实例。
  • 函数都是对象。
  • 函数都是Function的实例。
  • 对象都是Object的实例。
  • Object和Function都是Object和Function的实例。


接下来就揭开对象的神秘面纱吧。简单来说,对象就是一个字典:
var a = new Object();
a['x'] = 1;
a.y = 2;

a.x == 1;
a['y'] == 2;
new Object()是一个空对象(实际上就是{}),它是Object的实例。在这个例子中可以看到,当键名是一个合法的属性名时(不能以数字开头等),键值和这个对象的属性值是一回事。

再看看JavaScript是怎样自定义一个“类”的。
function 动物(名字) {
	this.名字 = 名字;
	this.叫什么 = function() {
		return '我叫' + this.名字;
	}
}

var 神马 = new 动物('神马');
神马.叫什么();
这里的new很关键,它表示将“动物”这个函数作为构造函数来初始化一个对象,而不仅仅是执行这个函数。具体来说,它会构造一个空对象{},然后将this指向这个对象,最后执行函数体。(其实还有一些其他的工作,这里先不提。)
如果没有new的话,就不会构造一个空对象,this也不会指向这个对象,而是指向全局对象(在浏览器里是window)。
因此,这段代码实际上相当于:
function 动物(名字) {
	this.名字 = 名字;
	this.叫什么 = function() {
		return '我叫' + this.名字;
	}
}

var 神马 = {};
动物.call(神马, '神马');
神马.叫什么();

顺便解释一下这里的call方法,它是将第一个参数作为this(如果没有的话就是null,于是会指向全局对象window),剩余的参数作为被调函数的参数来调用“动物”函数。
此外还有个类似的apply方法,举个例子应该就能弄懂了:
function f(a, b, c) {
	return this + a + b + c;
}

f.apply(1, [2, 3, 4]); // 10
f.call('1', 2, 3, 4); // "1234"

你应该会注意到,用它可以让对象和方法分离,于是可以用其他对象来调用并不属于它的方法:
var a = {};
a[0] = 1;
a[1] = 2;
a[2] = 3;
a.length = 3;

Array.prototype.join.apply(a, ['']); // "123"
Array.prototype.join.call(a, ''); // "123"
在这个例子中,我模拟了一个包含3个数的数组,然后想把数组中所有的数连接成一个字符串。
如果是原生的Array对象的话,是有join方法的,可我模拟的数组却没有, 于是便借用了Array.prototype.join方法。

此外,构造函数的返回值也很重要,如果它没有返回值(即返回值为undefined),或返回1、'1'和true等简单类型的字面量对象,那么new表达式的结果会是新构造出来的对象;但如果返回了一个对象(包括{}、new Number(1)和new String('1')等),那么new表达式的结果就是这个对象。
function A(x) {
	this.x = x;
	return x;
}

var a = new A(1);
a.x; // 1

a = new A({y: 2});
a.x; // undefined
a.y; // 2


原型

细心的话你应该会注意到我2次提到了prototype这个东西,却没有对其进行介绍。
实际上prototype是构造函数的一个属性。在new表达式初始化一个对象时,会将对象与构造函数的prototype相关联。当尝试访问对象的某个属性时,如果对象本身没有这个属性,那么会继续查找它的构造函数的prototype。
你有没有想过为什么所有的函数都有apply和call方法?这就是因为所有的函数都是Function的实例,而Function.prototype里定义了这2个方法。

接下来就给个例子:
function 动物(名字) {
	this.名字 = 名字;
}

动物.prototype.叫什么 = function() {
	return '我叫' + this.名字;
}

var 神马 = new 动物('神马');
神马.叫什么();

var 草泥马 = new 动物('草泥马');
草泥马.叫什么();

神马.叫什么 === 草泥马.叫什么; // true
这个例子和之前的很像,所不同的是我并没有定义“this.叫什么”,而是定义了“动物.prototype.叫什么”。而神马和草泥马本身并没有“叫什么”这个方法,于是在调用时,实际上是调用“动物.prototype.叫什么”。
这样的好处是不需要每个对象都重新定义一个自己实现来占用内存。此外,类属性也可以用prototype来实现。

了解了prototype的作用后,接下来就说说如何获取它吧。
在Firefox、Chrome和Safari等浏览器里,所有JavaScript对象都有个__proto__属性,这个属性就是它的构造函数的prototype(很显然,new一个对象的时候还需要做这件事)。不过这个属性并不是JavaScript标准中所定义的,并且已被摒弃。标准中建议的是使用Object.getPrototypeOf()方法,但这个方法是JavaScript 1.8.1才引入的,而且无法作为左值。
神马.__proto__ === 动物.prototype;
Object.getPrototypeOf(草泥马) === 动物.prototype;

另外,prototype还有个constructor属性,它默认指向构造函数自身。这个属性可以帮助我们知道一个对象是由哪个函数创建的。
神马.__proto__.constructor === 动物;
神马.constructor === 动物;
动物.prototype.constructor === 动物;

值得一提的是,__proto__和prototype都是可以动态更改的:
神马.__proto__ = {
	'叫什么': function() {
		return '我叫' + this.名字 + ',请多指教';
	}
};

动物.prototype.吃什么 = function() {
	return '我吃河蟹';
};

神马.叫什么(); // "我叫神马,请多指教"
神马.吃什么(); // TypeError: Object #<an Object> has no method '吃什么'
草泥马.叫什么(); // "我叫草泥马"
草泥马.吃什么(); // "我吃河蟹"

另外,prototype实际上也是一个对象,而这个对象也是有__proto__属性的,因此它还可以用来链式地实现继承。
function 动物(名字) {
	this.名字 = 名字;
}

动物.prototype.叫什么 = function() {
	return '我叫' + this.名字;
};

function 马(名字, 食物) {
	动物.call(this, 名字); // 调用父类的构造函数
	this.食物 = 食物;
}

马.prototype = new 动物(); // 需要复制动物.prototype到马.prototype
马.prototype.吃什么 = function() {
	return '我吃' + this.食物;
};

var 神马 = new 马('神马', '草');
神马.叫什么(); // "我叫神马"
神马.吃什么(); // "我吃草"

var 草泥马 = new 马('草泥马', '河蟹');
草泥马.叫什么(); // "我叫草泥马"
草泥马.吃什么(); // "我吃河蟹"
这段代码有2处要说明。
第一处是“动物.call(this, 名字)”。还记得之前所说的new的意义吗?这里就是将马函数里的this作为动物函数的this来调用,用来初始化父类。
第二处是“马.prototype = new 动物()”。实际上我们也可以用“马.prototype = 动物.prototype”,可是这样一来,改写马的prototype时,动物的prototype也会被改变。而如果用“马.prototype = new Object(动物.prototype)”,动物和马的prototype混在了一起,就不方便分开,也无法体现继承关系了。所以这里new了一个动物对象,这样就得到了它的__proto__属性。当要访问草泥马.叫什么时,草泥马并没有这个属性;于是查看草泥马.__proto__,发现仍然没有;于是再检查草泥马.__proto__.__proto__(其实就是动物.prototype),终于发现了叫什么,于是便调用这个方法了。
这个做法当然是有缺点的,例如constructor属性就不正确(手动赋值即可修正),而且代码看上去很繁琐。要解决这个问题,最好的方法就是使用现成的JavaScript库,例如MooTools和Prototype等。

顺带一提,实际上instanceof就是依靠这样的原型链来判断一个对象是否是一个函数的实例的:
var temp = 马.prototype.__proto__;
草泥马.__proto__.__proto__ = {};
草泥马 instanceof 动物; // false
草泥马 instanceof 马; // true
马.prototype.__proto__ = temp;
草泥马 instanceof 动物; // true

草泥马.__proto__ = {};
草泥马 instanceof 马; // false

草泥马.__proto__ = 动物.prototype;
草泥马 instanceof 动物; // true
草泥马 instanceof 马; // false

var prototype = {'__proto__': 马.prototype};
prototype instanceof 动物; // true
prototype instanceof 马; // true

草泥马.__proto__ = prototype;
草泥马 instanceof 动物; // true
草泥马 instanceof 马; // true

我想你应该已经发现其中的一个陷阱了。
对象的__proto__属性是它的构造函数的prototype,而prototype也是对象,它的__proto__属性是prototype的构造函数的prototype,也是个对象。这样一路追溯下去,由于对象肯定有__proto__属性,这就导致了查找可能会无限循环下去。
因此Object.prototype这个对象很特殊,它的__proto__属性为null,因此当检查到它后,就停止继续查找了。由于所有的对象都是Object的实例,因此不必担心查询陷入死循环。

此外,下述代码也验证了Object和Function之间的关系(是否到此才恍然大悟呢):
Object.__proto__ === Function.prototype; // Object是Function的实例
Function.__proto__ === Function.prototype; // Function是Function的实例
Object.__proto__.__proto__ === Object.prototype; // Object是Object的实例
Function.__proto__.__proto__ === Object.prototype; // Function是Object的实例

({}).constructor === Object;
Object.constructor === Function;
(function(){}).constructor === Function;
Function.constructor === Function;


我想到此应该没什么需要再解释的了,我也该去睡觉了=。=


2011年4月3日更新:推荐一下《JavaScript 秘密花园》这篇文章。

相关 [javascript 你懂的] 推荐:

JavaScript,你懂的

- dylan - keakon的涂鸦馆
经常有人问我,JavaScript应该怎么学. 先学基本语法,如果曾学过C等语言,应该1小时内就能掌握了. 再去使用内置的函数、方法和DOM API,熟悉它能干什么;而在学习DOM API的过程中,你还不得不与HTML和CSS打交道. 然后弄懂匿名函数和闭包,学会至少一个常用的JavaScript库(例如jQuery).

Javascript诞生记

- Milido - 阮一峰的网络日志
二周前,我谈了一点Javascript的历史. 今天把这部分补全,从历史的角度,说明Javascript到底是如何设计出来的. 只有了解这段历史,才能明白Javascript为什么是现在的样子. 我依据的资料,主要是Brendan Eich的自述. "1994年,网景公司(Netscape)发布了Navigator浏览器0.9版.

Javascript 里跑Linux

- rockmaple - Shellex&#39;s Blog
牛逼到暴的大拿 Fabrice Bellard,用Javascript实现了一个x86 PC 模拟器,然后成功在这个模拟器里面跑Linux(请用Firefox 4 / Google Chrome 11打开,Chome 12有BUG). 关于这个东西… 伊说 “I did it for fun“,大大啊大大啊….

高效 JavaScript

- xtps - ITeye论坛最新讨论
传统上,网页中不会有大量的脚本,至少脚本很少会影响网页的性能. 但随着网页越来越像 Web 应用程序,脚本的效率对网页性能影响越来越大. 而且使用 Web 技术开发的应用程序现在越来越多,因此提高脚本的性能变得很重要. 对于桌面应用程序,通常使用编译器将源代码转换为二进制程序. 编译器可以花费大量时间优化最终二进制程序的效率.

你得学JavaScript

- 蒋冰 - 伯乐在线 -博客
  注:本文由 敏捷翻译 - 蒋少雄 翻译自 Kenny Meyers 的博文.   如果三年前你问我应该学什么语言,我会告诉你是Ruby.   如果你现在想学一门语言的话,你应该学习JavaScript..   我认为,每一位Web开发人员都应该学习JavaScript. 目前推出的许多新技术都支持这个观点.

javascript 贪食蛇

- Xin - 博客园-首页原创精华区
我的程序用javascript与Html中的table结合,实现的简单的贪食蛇游戏,游戏的主要特点,可调整蛇移动速度,可调整蛇移动范围,碰壁、咬到身体则“Game Over. 游戏并不完善,只是实现了主要的功用,有设计不合理的地方,欢迎您感大家提意见.        实现方法:由javascript语言中的setInterval方法驱动整个游戏程序,设置“nowDirection”即蛇的当前移动方向为全局变量,由setInterval方法定时获取蛇的移动方向,由document.onkeydown()捕捉当前按键(上、下、左、右)以修改nowDirection,这样就可以用方向按键控制蛇周期时间的定向移动.

你不懂Javascript

- 英建 - 黑客志
过去几年我注意到技术圈一个很奇怪的现象,有太多程序员将那些他们只是有过非常浅显的了解,但其实根本就不懂的技术写到他们的简历中,这个现象几乎每种语言都有,但这其中最严重的就要数Javascript了. 出现这种状况的一个很大的原因就是现如今几乎每个开发者的工作都或多或少要依赖于Javascript,但大多数人并不真的理解这门语言,他们常用的学习方式是复制粘贴,使用这种方式,你永远不会真正学会这门语言,而只能得到一个你已经懂了的假象.

Javascript 中的 var

- - 酷壳 - CoolShell.cn
MelonCard发布了一篇文章——” how one missing var ruined our launch“(”少写了一个var毁了我的网站”),这篇文章是说MelonCard用Node.js做后台,因为出了一个小高峰——有50-100人注册,结果整个网站都不响应了,而且还出现了很多奇怪的问题.

JavaScript 基础

- - CSDN博客推荐文章
JavaScript可以出现在HTML的任意地方,甚至在之前插入也不成问题,使用标签. 不过要在框架网页中插入的话,就一定要在之前插入,否则不会运行. JavaScript对数据类型要求不严格,一般来说,声明变量的时候不需要声明类型,而且就算声明了类型,在使用过程中也可以给变量赋予其他类型的值,声明变量使用var关键字.

javaScript DOM使用

- - CSDN博客互联网推荐文章
通过 HTML DOM,可访问 JavaScript HTML 文档的所有元素. 1 修改HTML元素内容. document.write(Date()); //在输入流中直接写 document.getElementById(id).innerHTML=new HTML. //改变已经有的元素内容 document.getElementById("image").src="landscape.jpg";.