转自:http://www.ituring.com.cn/article/details/996?q=javascript
若想成为一个高效的JavaScript开发者,其秘诀之一就是真正理解这门语言的语义。本文将会通过通俗易懂的图表来解释JavaScript中最基本的核心内容。
随处可见的引用
JavaScript中的变量其实是一个标签,它引用了内存中某个位置的某个值。这些值可以是字符串、数字和布尔值的原始值。它们也可以是对象(object)或函数(function)。
本地变量
在下面这个例子中,我们会在顶级作用域中创建四个本地变量,并将它们指向某个原始值。
// 我们在顶层作用域创建一些本地变量
var name = "Tim Caswell";
var age = 28;
var isProgrammer = true;
var likesJavaScript = true;
// 测试一下看看两个变量是否引用了相同的值
isProgrammer === likesJavaScript;
输出 => true
注意两个布尔值指向的是内存中的同一位置,这是因为原始值是不变的,因此虚拟机(JavaScript解释器 ——译者注)可以优化它们,使所有的引用共享这个原始值的同一实例。
在这个代码片段中,我们使用 === 来判断两个引用是否指向同一个值,得到的结果是 true。
外面的框代表最外层的封闭作用域。这些变量是最顶级的本地变量,不要把它们跟global/window对象的属性相混淆了。
对象和原型链
对象只不过是更多引用的集合,它们指向新创建的对象和原型。它们唯一增添了一点比较特殊的特性就是原型链(Prototype Chains),当你试图访问一个不属于本地对象而属于其父对象的属性时就会用到原型链。
// 创建一个父对象
var tim = {
name: "Tim Caswell",
age: 28,
isProgrammer: true,
likesJavaScript: true
}
// 创建一个子对象
var jack = Object.create(tim);
// 覆盖一些本地属性
jack.name = "Jack Caswell";
jack.age = 4;
// 通过原型链进行查找
jack.likesJavaScript;
输出 => true
这里,我们有一个包含四个属性的被 tim 变量所引用的对象,同时我们创建了一个新的对象,该对象继承自第一个对象并且引用为 jack,然后我们覆盖了本地对象的两个属性。
现在,当我们查找jack.likesJavaScript时,起初会找到了jack所引用的对象,然后会继续查找likesJavaScript属性。由于本地对象中并不包含该属性,因此我们查找其父对象并找到了该属性,最后则找到了该属性所指向的true这个值。
全局对象
你想知道为什么像jslint这种工具经常会提示你别忘了在变量的前面增加var声明吗?好吧,我们看看如果丢掉的话会发生什么情况。
var name = "Tim Caswell";
var age = 28;
var isProgrammer = true;
// 不小心丢掉了var
likesJavaScript = true;
注意,现在likesJavaScript已经是全局对象的一个属性,而不是外层封闭作用域中的一个自由变量了。尽管这种情况只有在混搭几段脚本时才会有问题,不过,在任何真正的程序中都会出现混搭的情况。
请牢记一定要添加这些var声明,这样才能保证你的变量是在当前的作用域及其子对象的作用域中。遵循这个简单的规则将使你受益匪浅。
如果你必须要在全局对象上添点儿东西,那么在浏览器中就明确地使用window.woo,而在node.js中则使用global.goo。
函数与闭包
JavaScript并不只是一系列的链式数据结构,它还包含了被称作函数(function)的可执行可调用代码。这些函数会创建链式作用域和闭包(closure)。
可视化的闭包
函数可以被看作是包含可执行代码及属性的特殊对象。每个函数都有一个特殊的[scope]属性,它代表了函数被定义时的环境。如果一个函数是由另外一个函数返回的,则这个指向旧环境的引用就会在一个“闭包”中被新的函数所终止。
在这个例子中,我们会创建一个简单的工厂方法,它可以生成一个闭包并返回一个函数。
function makeClosure(name) {
return function () {
return name;
};
}
var description1 = makeClosure("Cloe the Closure");
var description2 = makeClosure("Albert the Awesome");
console.log(description1());
console.log(description2());
输出 Cloe the Closure Albert the Awesome
当我们调用description1()时,虚拟机会查找它所引用的函数并执行。由于这个函数会查找一个名为name的本地变量,因此它会在闭包作用域中进行查找。这个工厂方法的好处就是,每个生成的函数都有自己的本地变量空间。
参见这篇“为什么使用闭包(why use closure)”来获得更多关于闭包及其使用的内容。
共享的函数和this关键字
有时由于性能的原因,或者因为就是喜欢这种风格,JavaScript提供了一个this关键字允许你在不同的作用域中依据函数被调用的形式来重用函数对象。
这里我们创建一些对象,它们全部共享同一个函数,这个函数会在内部引用this来展示调用过程中的变化。
var Lane = {
name: "Lane the Lambda",
description: function () {
return this.name;
}
};
var description = Lane.description;
var Fred = {
description: Lane.description,
name: "Fred the Functor"
};
// 从四个不同的作用域调用函数
console.log(Lane.description());
console.log(Fred.description());
console.log(description());
console.log(description.call({
name: "Zed the Zetabyte"
}));
输出:Lane the Lambda Fred the Functor undefined Zed the Zetabyte
在此图中,我们看到即使Fred.description被设置成Lane.description,它实际上也只是引用了函数本身。因此,三个引用都同样对匿名函数拥有所有权。这就是为什么我没有在构造原型上通过“method”来调用函数的缘故,因为这意味着函数与其构造器和它的“类”之间的某种绑定关系。(详见“什么是this”what is this 获得更多关于this的动态本质的细节)
我很乐于用图表来使这些数据结构可视化,我希望这些内容可以帮助我们这些视觉学习者更好地掌握JavaScript的语义。我曾有过前端开发/设计和服务器端架构的经验。我希望我独特的视角能够帮助那些从设计岗位过来,并想深入学习这门被称作JavaScript的美妙语言的同学。
由于我的第一篇文章里通过图解描述JavaScript语义的方式大受欢迎,因此我决定尝试用这种方法来讲解一些高级内容。在本文中,我会讲解三种常用的创建对象的技术,它们分别是:构造器(constructor)加原型(prototype)的方式、纯原型的方式以及对象工厂(object factory)的方式。
我的目的是希望能够帮助大家理解每种技术的优缺点,并理解其运行机理。
经典的JavaScript构造器
首先我们通过原型来创建一个简单的构造器。这是在原生的JavaScript中最接近类(class)的一种方式。它非常强大而有效,但是我们并不能奢望它像其他包含类的语言一样强大。
//长方形
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype.getArea = function getArea() {
return this.width * this.height;
};
Rectangle.prototype.getPerimeter = function getPerimeter() {
return 2 * (this.width + this.height);
};
Rectangle.prototype.toString = function toString() {
return this.constructor.name + " a=" + this.getArea() + " p=" + this.getPerimeter();
};
//正方形
function Square(side) {
this.width = side;
this.height = side;
}
Square.prototype.__proto__ = Rectangle.prototype;
Square.prototype.getPerimeter = function getPerimeter() {
return this.width * 4;
};
//测试
var rect = new Rectangle(6, 4);
var sqr = new Square(5);
console.log(rect.toString())
console.log(sqr.toString())
现在我们新定义一个叫做Square的类对象,它继承自Rectangle。为了实现继承,构造器的prototype必须继承自父构造器的prototype。这里我们覆盖了getPerimeter使其更加高效,顺便展示一下如何来覆盖函数。
function Square(side) {
this.width = side;
this.height = side;
}
Square.prototype.__proto__ = Rectangle.prototype;
Square.prototype.getPerimeter = function getPerimeter() {
return this.width * 4;
};
用法就很简单了,只要给每个都创建一个实例(instance)并在实例上调用函数即可。
var rect = new Rectangle(6, 4);
var sqr = new Square(5);
console.log(rect.toString())
console.log(sqr.toString())
输出:
Rectangle a=24 p=20 Square a=25 p=20
下图是生成的数据结构,虚线表示对象的继承。
注意,虽然它们都是继承自Rectangle.prototype的对象,但在rect实例和Square.prototype之间还是有一点小区别。如果你仔细研究的话,会发现JavaScript不过是一系列相互关联的对象而已。唯一特殊的对象就是函数(function)了,在函数中可以接受参数并且可以包含可执行的代码,函数还可以指向作用域(scope)。
纯原型对象
再看刚才的例子,这次我们不使用构造函数,而只使用纯原型继承。
我们来定义一个Rectangle原型来作为构建其他对象的基础。
var Rectangle = {
name: "Rectangle",
getArea: function getArea() {
return this.width * this.height;
},
getPerimeter: function getPerimeter() {
return 2 * (this.width + this.height);
},
toString: function toString() {
return this.name + " a=" + this.getArea() + " p=" + this.getPerimeter();
}
};
现在我们来定义一个名为Square的子对象,并且覆盖一些属性来改变它的某些行为。
var Square = {
name: "Square",
getArea: function getArea() {
return this.width * this.width;
},
getPerimeter: function getPerimeter() {
return this.width * 4;
},
};
Square.__proto__ = Rectangle;
为了创建这些原型的实例,首先我们简单地创建一个继承自原型对象的新对象,然后再手动设置一些局部状态。
var rect = Object.create(Rectangle);
rect.width = 6;
rect.height = 4;
var square = Object.create(Square);
square.width = 5;
console.log(rect.toString());
console.log(square.toString());
输出:
Rectangle a=24 p=20 Square a=25 p=20
下面是生成的对象图:
这个方法没有构造器+原型的方法那么强大,但是通常更容易理解一点,因为它没有那么拐弯抹角。当然了,如果你之前使用的语言包含纯原型继承,那么你会很高兴地发现在JavaScript中也是可以实现的。
对象工厂
我最喜欢的创建对象的方法之一就是使用工厂函数。它的不同之处在于,你不必定义包含所有共享函数的原型对象,然后再创建这些对象的实例,每次只需要简单地调用一个可以返回新对象的函数即可。
这个例子是一个超简单的MVC系统。控制器(controller)函数接受作为参数的模型(model)和视图(view)对象并且输出一个新的控制器对象。所有状态都通过作用域保存在闭包中。
function Controller(model, view) {
view.update(model.value);
return {
up: function onUp(evt) {
model.value++;
view.update(model.value);
},
down: function onDown(evt) {
model.value--;
view.update(model.value);
},
save: function onSave(evt) {
model.save();
view.close();
}
};
}
若想使用该函数,只需要传入所需的参数调用函数即可。注意一下我们是如何用它来作为事件处理函数(setTimeout)而不用事先将函数绑定到对象上的。由于它(该函数)在内部不使用this关键字,因此就没有必要搞乱this的值了。
var on = Controller(
// 内嵌模拟的模型
{
value: 5,
save: function save() {
console.log("Saving value " + this.value + " somewhere");
}
},
// 内嵌模拟的视图
{
update: function update(newValue) {
console.log("View now has " + newValue);
},
close: function close() {
console.log("Now hiding view");
}
}
);
setTimeout(on.up, 100);
setTimeout(on.down, 200);
setTimeout(on.save, 300);
// 输出
View now has 5
View now has 6
View now has 5
Saving value 5 somewhere
Now hiding view
下面是这段代码生成的对象图。注意我们是通过函数隐藏的[scope]属性来访问传入的两个匿名对象的,或者换句话说,我们通过工厂函数创建的闭包可以访问到model和view。
结论
这里面有太多我想探索的细节了,不过我更喜欢保持文章的简短易读。如果大家有需求的话,我会再写第三篇文章来讲解如何使用ruby风格的mixin以及其他一些高级内容。
原文链接:http://howtonode.org/object-graphs-2