挑战与解决方案 - 现代 Web 应用程序架构 - JavaScript - 第 2 部分






4.87/5 (38投票s)
JavaScript 是现代 Web 应用程序的重要组成部分。本文将概述 JavaScript 的一些功能。
文章系列
- 挑战与解决方案 - 现代 Web 应用程序架构 - 第一部分
- 挑战与解决方案 - 现代 Web 应用程序架构 - JavaScript - 第二部分
- 挑战与解决方案 - 现代 Web 应用程序架构 - 移动应用 - 第三部分
引言
本系列的第一部分展示了一些 CSS 3 和 JavaScript 的实际应用。现在,我们将更多地了解设备端的“忍者”,即 JavaScript。我们将从电池寿命、计算能力和存储空间有限的设备的角度来看待挑战和解决方案。
图 - 你想在哪里运行 JavaScript?
JavaScript 现状
JavaScript 是一种强大的语言,人们以各种风格编写代码。市面上有大量的脚本,脚本重用对于进步至关重要,无论它来自Brendan Eich、Douglas Crockford、Ryan Dahl,还是其他人。为了合并现有框架,理解正在使用的各种风格非常重要。
图 - 从左到右:JavaScript 的创建者Brendan Eich、在 YUI 和 JSON 中做出重大贡献的Douglas Crockford,以及 Node.js 的创建者Ryan Dahl
在我继续之前,每当你遇到一个解释不清楚的例子时,我都建议在jsfiddle中进行尝试!:)
类和对象
在 JavaScript 中,你通过声明其构造函数来声明一个类,而构造函数“不过是”一个“常规”的 JavaScript 函数。
function Button() {} // A Button class delcared
在 JavaScript 社区中,使用驼峰命名法(Title Case)来命名构造函数是一种约定。上面的代码声明了两件事:一个类 Button
,以及它的构造函数。使用这个函数(或者如果你仍然想区分的话,构造函数),你可以使用new运算符创建 Button
对象。
var button1 = new Button();
var button2 = new Button();
类只是一个常规函数
上面的代码将分配两个内存位置,并执行 Button()
函数内的代码两次。如前所述,底层 Button
的“类”或“构造函数”不过是一个“常规”的 JavaScript 函数。事实上,“每个”JavaScript 函数都是Function
类的一个实例。
var add = new Function("a", "b", "return a + b");
add(2, 6); // call the function
// As function is object it can be used like a 'regular object'
// you can return function from a function or send it as argument
// and save it in a var
function addFactory() {
var addMethod = function (a, b) {
return a + b;
}
return addMethod;
}
var x = addFactory();
alert(x(1, 2)); // return 3
事实上,JavaScript 中的所有对象都继承自 Object
;并从 Object.prototype
继承方法和属性,尽管它们可能会被覆盖。
typeof Button; // returns "function"
typeof button1; // returns "object"
button1 instanceof Button // true
带参数的构造函数
你可以像普通 JavaScript 函数一样,拥有一个带有参数的构造函数。
function Button(text) {
}
类资产 - 方法和成员
一个类有两种资产:成员和方法。在下面的例子中,我们将逐步向 Button
类添加私有、特权、公共和静态资产。我们还将初步了解继承和访问规则。
public
拥有类的公共成员
function Button(text) {
this.text = text; // public member
}
var button = new Button("Submit");
button.text; // return 'Submit'
私有的
以及拥有一个私有成员,即一个在公共方法中不可访问的成员。
function Button(text) {
var visible; // private member
}
特权
并添加一个特权方法,即一个可以访问私有成员的方法。
function Button(text) {
var visible;
this.show = function() { // privileged method
visible = true;
};
}
重要的是要记住,私有成员是在不使用 this
的情况下声明和访问的。
私有方法
添加私有方法 hide()
。
function Button(text) {
var visible;
function hide() { // private method
visible = false;
}
}
公共方法
并添加一个公共方法 resetText()
和一个公共成员 rounded
。
function Button(text) {
this.text = text; // public member
var visible; // private member
this.show = function() { // privileged method
visible = true;
};
function hide() { // private method
visible = false; // INCORRECT: this.visible = false; do not use 'this' for private member
}
}
Button.prototype.rounded = false; // public member
Button.prototype.resetText = function() { // public method can not access private assets
this.text = ""; // INCORRECT: text = ""; always use 'this' for public member
}
公共和特权之间的关键区别在于,特权方法是为类的每个实例创建的,而公共方法则在类的所有实例之间共享。这在创建数千个对象时很重要。我们稍后将对此进行更多讨论。
static
并添加一个静态成员或方法。
function Button(text) {
...
}
Button.defaultText = "My Button"; // static member
Button.resetDefaultText = function() { // static method
Button.defaultText = "";
};
继承(或链式调用)
图 - JavaScript 在所有链式对象中搜索方法或成员。
如前所述,每个 JavaScript 类只是一个常规函数。我们还知道,JavaScript 中的每个函数都是Function
类的一个实例。Function
类有一个公共属性prototype,用于将对象链式连接起来。当一个子对象被链式连接到父对象时,所有非私有父资产都表现得好像它们是子对象的一部分。如果子对象中没有可用的方法或成员,JavaScript 会在其父原型中查找,并且此搜索会一直持续到找到一个prototype = null 的对象。请查看此示例。
function A() {
this.num = 100;
}
function B() {
}
function C() {
}
B.prototype = new A(); // A is parent of B
C.prototype = new B(); // B is parent of C
var x = new C()
x.num; // return 100 search C-->C.prototype-->B.prototype-->A
x.speed; // return undefined search C-->C.prototype-->B.prototype-->A.prototype-->Object.prototype-->null
你可以看到,链式调用可以用于创建继承,如下所示。但在我们这样做之前,我想指出,深度链式调用可能会因为搜索而影响性能。此外,对于不存在的成员,搜索将一直持续到链的末端。
使用 new 继承
此示例展示了如何使用 new
从 Button
类继承。
function Button(text) {
this.text = text;
}
function LinkButton(text, url) {
Button.call(this, text); // call base class constructor
this.url = url;
}
LinkButton.prototype = new Button(); // this will make Button a base class
var b = new LinkButton("Google", "http://www.google.com");
b instanceof Button; // return true
b instanceof LinkButton; // return true
b.text; // calling base class public member
使用 Object.create() 继承
图 - 大对象可以稍后使用 Object.call(this) 初始化。
new
运算符不仅创建 Button
对象,还调用其构造函数。在上例中,Button
构造函数被调用了两次,一次在 new Button()
,然后是 Button.call(this, text)
。这浪费了 CPU 周期。更好的方法是使用 Object.create()
,它只创建对象而不调用构造函数。您之后可以使用 Object.call()
调用基类构造函数。
function Button(text) {
this.text = text;
}
function LinkButton(text, url) {
Button.call(this, text); // call base class constructor
this.url = url;
}
LinkButton.prototype = Object.create(Button); // only create object, do not call Button constructor
var b = new LinkButton("Google", "http://www.google.com");
b instanceof Button; // return true
b instanceof LinkButton; // return true
b.text; // calling base class public member
嵌套与原型
现在是时候提到,Button
构造函数中的代码应该简短(并快速执行),以减少对象创建时间。考虑在智能手机上创建数百个 LinkButton
对象并将其添加到列表中的情况。在智能手机上,快速的对象创建+初始化将消耗更少的 CPU 周期,从而延长电池寿命,而电池寿命通常为 6 到 10 小时!
图 - 在电池寿命为 6 到 10 小时的 iPhone 上运行 JavaScript。
除了 CPU 使用导致的电池消耗外,嵌套在“构造函数”中的函数是为每个类的新实例创建的,这将占用内存。
图 - 智能手机上的典型 RAM。
由于嵌套函数是为每个对象创建的,请尽可能将公共资产(即方法和成员)和初始化放在构造函数之外。这将使对象创建过程更快,并且占用的空间更小。
// Good
function Button(text) {
this.text = text;
}
Button.prototype.resetText = function() { // public method
this.text = "";
};
// Bad
function Button(text) {
this.text = text;
this.resetText = function() { // PROBLEM: public method declared as privilaged
this.text = "";
};
}
仅当它访问私有类资产时,或者当构造函数用提供的参数初始化成员时,才在构造函数内部声明方法。在所有其他情况下,请使用prototype来添加类资产。
尽管近期的智能手机提供了“桌面级”CPU,但嵌套会创建作用域级别,并减慢名称解析速度。
类和对象 - 动态添加或删除资产
尽管 JavaScript 中的类充当对象创建的“蓝图”,但是您可以在创建对象之后添加方法和成员。“即时”类资产会立即添加到该类的“所有活动”对象中,而“即时”对象资产只会添加到“那个特定”对象实例中。例如,请考虑以下内容。
function Button(text) {
this.text = text;
}
var b1 = new Button("Tooltip Button");
var b2 = new Button("Normal Button");
b1.toolTip = "This tooltip public member is only for object 'b1'"; // on-the-fly 'object asset'
b2.toolTip; // ERROR: undefined
Button.prototype.resetText = function() { // on-the-fly 'class asset'
this.text = "";
};
b1.resetText(); // OK
b2.resetText(); // OK too
就像您可以向类或对象添加资产一样,您也可以删除它们。
delete b1.toolTip;
Button.prototype.resetText = undefined;
b1.toolTip; // undefined
b2.resetText(); // ERROR
即时资产管理可能有助于使用异步模块定义 (AMD) 在线组合跨越多个 JavaScript 文件的对象。即使对于较小的对象,组合也可能有用,例如设备功能检测和必要的“修剪”对象。
封锁和冻结对象(非类)
当我们谈论 JavaScript、JScript 和 ECMAScript 时,它们是1 种语言还是 3 种?我想简要地触及这个主题,所以可以安全地说 ECMAScript 5 有“一种方法”来关闭对象中的即时资产添加和删除。
JavaScript 是一种由 Netscape 支持(现在是 Mozilla 支持)的语言,而 JScript 是微软开发的类似版本。1996 年,微软和 Netscape 同意基于 ECMA 标准开发他们各自的语言版本,称为ECMAScript或简称ES。
在 ES5(或 JavaScript 1.8.5)兼容的浏览器中,您可以Seal()
和Freeze()
一个对象。您无法在Sealed对象中添加或删除属性,而Freezing在使属性值不可编辑的同时,增加了密封的功能。如果您只想停止添加新属性,但现有属性可以删除,请使用preventExtensions()
。Seal、Freeze 和 Prevent Extensions 不适用于类。请考虑以下示例。
function Button(text) {
this.text = text;
}
var b = new Button("My Button");
Object.seal( b );
Object.isSealed( b ); // returns true
b.text = "Submit"; // OK
b.toolTip = "Mine"; // FAIL silently
delete b.text; // FAIL silently
Button.prototype.toolTip = "Mine"; // OK, only object is sealed not Button class
Object.freeze( b );
Object.isFrozen( b ); // returns true
b.text = "Submit"; // FAIL silently
delete b.text; // FAIL silently
Button.prototype.toolTip = "Mine"; // OK, only object is freezed not Button class
Object.preventExtensions( b );
Object.isExtensible( b ); // returns false
b.text = "Submit"; // OK, you can still edit
delete b.text; // OK too
b.toolTip = "Mine"; // FAIL silently
Button.prototype.toolTip = "Mine"; // OK, only object is prevents extentions not Button class
资产的哈希表性质
JavaScript 属性可以使用“索引”表示法、“键”表示法和“点”表示法声明。在不同上下文中,它们都是有用的构造。请参见下面的示例。
function Button(text) {
this[0] = text;
this["is enabled"] = true;
this.focused = false;
}
var b1 = new Button("My Button");
b1[0]; // return "My Button". Mostly used in loops
b1["is enabled"]; // return true. Mostly used for space separated, hypen'ed properties
b1.focused; // good ol dot notation return false
that
在 JavaScript 中,私有方法无法访问特权资产。以下操作将不起作用。
function Button(text) {
this.text = text; // privilaged member
function isEmpty() {return this.text === ""; } // ERROR, this is not available in private methods
}
为了提供必要的访问权限,JavaScript 社区使用了一个名为 'that
' 的变量作为变通方法。
function Button(text) {
this.text = text;
var that = this; // store this in private var 'that'
function isEmpty() {return that.text === ""; } // 'that' will work as it is a private member
}
一个更完整的例子可能是。
function Button(text) {
this.text = text;
var that = this;
function isEmpty() {return that.text === ""; }
this.getText = function() { return isEmpty() ? "NO TEXT" : this.text; };
}
var b1 = new Button("");
b1.getText(); // return "NO TEXT"
ASI - 自动分号插入
JavaScript 将“每行”视为一个新语句,除了少数情况。这意味着您不必在每行末尾添加分号,因为 JavaScript 会自动完成这项工作。这称为ASI或自动分号插入。因此,以下是一个完全有效的脚本。
function Button(text) {
this.text = text
var that = this
function isEmpty() {return that.text === "" }
this.getText = function() { return isEmpty() ? "NO TEXT" : this.text }
}
var b1 = new Button("")
b1.getText() // return "NO TEXT"
对于 return、throw、break 或 continue,JavaScript 会自动为新行插入分号。// ASI if line ends with 'return'
function sum(a,b) { return // ASI here return;
a+b }
sum(1,3) // return undefined
// ASI if line ends with 'throw'
throw // ASI here throw;
"an error"
// ASI if line ends with 'break'
var day=0
switch (day)
{
case 0:
x = "Sunday"
break // ASI here
case 1:
x = "Monday"
break
case 2:
x = "Tuesday"
break
case 3:
x = "Wednesday"
break
case 4:
x = "Thursday"
break
case 5:
x = "Friday"
break
case 6:
x = "Saturday"
break
}
alert(x) // return "Sunday"
// ASI if line ends with 'continue'
continue // ASI here continue;
以下是 JavaScript 将新行视为前一行的延续的地方,因此 JavaScript 不会自动为此类情况插入分号。// No ASI for unclosed paren
function onclick(
e)
// No ASI for unclosed array literal
var states = [
"NY", "MA", "CA"]
// No ASI for unclosed object literal
var states = {
name : "Newyork", code : "MA"}
// No ASI if ends with comma
var states,
cities
// No ASI if ends with dot
from("state").
where("id='MA'").
select()
// No ASI if ends with ++ or --
var i = 1, j=5
i
++
j
// here i = 1 and j = 6
// No ASI for 'for'
for(var i = 0; i < 2; i++)
alert("hello") // part of loop
alert("world") // not a part of loop
// No ASI for 'while'
var i = 0;
while(i++ < 2)
alert("hello") // part of loop
alert("world") // not a part of loop
// No ASI for 'do'
var i = 0;
do
alert("hello") // part of loop, to put 2 or more statements use {}
while (i++ < 2)
// No ASI for 'if'
var i = 0
if(i == 0)
alert("hello") // alert will popup
// No ASI for 'else'
var i = 1;
if(i == 0)
alert("hello")
else
alert("world") // alert will popup
没有类的对象
JavaScript 是一种可以创建没有类的语言。其中最简单的是一个“空”对象,即没有任何资产的对象。您可以随时向空对象添加资产,也可以声明带有资产的对象。JavaScript 中的{}
代表一个空对象或空对象字面量。
// empty object var b={};
现在添加一些属性。
// object with properties var b={text:"My Button", visible:false}; b.text; // return "My Button"
并添加一个方法。
// object with method
var b = {text:"My Button", visible:false, show: function() {this.visible = true;} };
b.show();
b.visible; // returns true
您可以使用对象字面量符号从函数返回一个对象。
// a function returning object
function getButton() {
return {
text: "My Button",
visible: false,
show: function () {
this.visible = true;
}
}
}
getButton().text; // return "My Button"
您也可以嵌套对象。
// nested objects
var toolbar = {
button1: {text: "My Button 1"}
,button2: {text: "My Button 2"}
}
toolbar.button2.text; // return "My Button 2"
命名空间 - 避免命名冲突
正如您所料,您的脚本将与其他脚本一起运行,因此将代码保存在一个命名空间中非常重要。到目前为止,我们所有的类都写在全局作用域中。让我们创建几个命名空间。
// create namespace. assign already created 'com' object else where or empty object {}
var com = com || {};
com.Acme = com.Acme || {};
com.Acme.UI = com.Acme.UI || {};
com.Acme.UI.Button = com.Acme.UI.Button || {};
现在我们有了 com.Acme.UI
命名空间,并且 Button
类已添加,现在是时候添加类资产了。但首先请注意,您不能在全局命名空间中创建构造函数 function Button()
。所以,让我们看看您可以用匿名函数做的一些有趣的事情。
// an anonymous function declaration
function () {}
上面的代码将失败,因为。
- JavaScript 不期望匿名函数。
- JavaScript 期望语句的左侧和右侧都有内容。
另外,JavaScript 语句的最简单形式是。
// a semi
;
让我们将匿名函数括在括号中,使其成为一个语句。
// a function statement
(function () {});
让我们回到我们的 Button
类。
// adding paren to make it a 'function statement'
com.Acme.UI.Button = (function () {});
如果在匿名函数末尾加上括号,它将立即执行。
// adding () to immidiately execute anonymous function
com.Acme.UI.Button = (function () {}());
现在我们有了一个匿名“容器”来保存我们的类。让我们添加一些“花哨”的功能。
// split one liner anonymous container in multi-line
com.Acme.UI.Button = (function () {
}());
// add constructor
com.Acme.UI.Button = (function () {
function Button(text) {
}
}());
// export Button class (simply return Button)
com.Acme.UI.Button = (function () {
function Button(text) {
}
return Button;
}());
// add public property
com.Acme.UI.Button = (function () {
function Button(text) {
this.text = text;
}
return Button;
}());
// add public, private and privileged assets
com.Acme.UI.Button = (function () {
// constructor
function Button(text) {
this.text = text; // public property
var that = this;
// private method
function isEmpty() {
return that.text === "";
}
// privileged method
this.getText = function () {
return isEmpty() ? "NO TEXT" : this.text;
};
}
// public method
Button.prototype.resetText = function () {
return this.text = "";
}
return Button;
}());
var b = new com.Acme.UI.Button("My Button");
b.getText(); // return "My Button"
b.resetText();
b.getText(); // "return "NO TEXT"
库或模块
您可以使用前面部分中使用的“匿名容器”来创建跨越多个文件的 JavaScript 库。库或模块提供了一个作用域,您可以在其中拥有公共和私有成员。
var LIBRARY = (function () {
// all global variables will be available here
// however assets in this scope will not conflict
// with global scope as they are private
});
var MODULE = (function (jQuery, underscore) {
// import libraries with different name here
// to avoid any conflicts. Access to imported
// variable will be faster as compared to
// global access
}($, _));
我们可以创建简单的Util库,如下所示。
var Util = (function (u) {
// private
var key = "secret";
// private method
function hash() {
return 123;
}
// public property
u.version = "1.0";
// public method
u.productName = function () {
return "Acme Library";
}
return u;
}(Util || {}));
Util.productName(); // return "Acme Library"
以下是隐藏私有状态并导出公共方法的另一种方法。请注意,在调用匿名函数时会返回一个对象。
var Counter = (function () {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
}
}
})();
Counter.increment();
alert(Counter.value()); // return 1
方法链式调用
方法链式调用使您能够一个接一个地调用对象上的方法。如果您不返回任何内容,则方法返回this
。
var obj = {
value: 1,
increment: function () {
this.value += 1;
return this;
},
add: function (v) {
this.value += v;
return this;
},
shout: function () {
console.log(this.value);
}
};
// chain method calls
obj.increment().add(3).shout(); // 5
// As opposed to calling them one by one
obj.increment();
obj.add(3);
obj.shout();
简洁的 getter 和 setter
JavaScript 通过 get
和 set
为属性 get 和 set 方法提供了简洁的语法。
var carStero = {
_volume: 0,
get volume() {
return this._volume;
},
set volume(value) {
if (value < 0 || value > 100) {
throw new Error("Volume shold be between 0-100");
}
this._volume = value;
}
};
try {
carStero.volume = 110;
} catch (err) {
alert(err.message);
}
全局变量
图 - 最大限度地减少全局空间的使用。
您可以非常容易地声明全局变量。
var width = 100; // global
height = 50; // global without var
delete width; // ERROR
delete height; // OK
var states = ["CA", "NY", "MA"];
delete states[1]; // OK, delete NY
states[1]; // return undefined
states.length; // return 3
当delete
运算符删除数组元素时,该元素就不再位于数组中,访问它将返回undefined。但是,它仍然是可寻址的。
任何没有var
的成员都会进入全局作用域。
height; // global
function Button(text) {
var label = caption = text; // 'label' is private but without var 'caption' is global
caption = text; // without var 'caption' is global
}
ECMAScript 5 严格模式
John Resig 对 ES5 严格模式进行了出色的解释。他这样说:
"use strict";
在严格模式下,删除变量、函数或参数将导致错误。
var foo = "test";
function test() {}
delete foo; // Error
delete test; // Error
function test2(arg) {
delete arg; // Error
}
在对象字面量中多次定义属性或函数参数将导致抛出异常。
// ERROR
{
foo: true,
foo: false
}
function (foo, foo) {} //ERROR
几乎所有尝试使用名称 'eval
' 的行为都被禁止——并且无法将 eval
函数分配给变量或对象的属性。
// All generate ERROR
obj.eval=...
new Function("eval")
此外,通过 eval
引入新变量的尝试将被阻止。
eval("var a=false;");
print( typeof a ); // undefined
最后,一个长期存在的(并且非常烦人的)错误已经得到解决:null 或 undefined 被强制转换为全局对象的情况。严格模式现在可以防止这种情况发生,并会抛出异常。
(function(){ ... }).call( null ); // Exception
当启用严格模式时,with(){}
语句将失效——事实上,它甚至会显示为语法错误。
函数参数
有一个arguments
对象,它仅在函数体内可用。这可能有助于创建多个类构造函数或实用函数,这些函数会遍历所有arguments
来执行某项任务,例如排序。
function sort() {
alert(arguments.length);
arguments[1] = 5; // 1, 5, 3
alert(arguments[1]);
}
sort(7,2,3);
sort("A","B","C", "D");
闭包
随着嵌套 JavaScript 函数的出现,出现了闭包的能力,即在函数返回后仍然保留堆栈上的变量。在正常情况下,当函数返回时,它的堆栈会被销毁,所有变量也会如此。对于一个典型的函数,一旦它返回到调用环境,它所有的局部变量和参数都符合垃圾回收的条件。
当一个函数的局部变量或函数参数在函数返回后仍然保留时,就会发生闭包。考虑以下返回匿名函数的函数。匿名内部函数会记住min
的值在它返回时是什么,即使内部函数在代码后面被调用。
function volumeFactory(min) {
return function (value) {
return value + min;
}
}
var mode = "meeting";
var funcSetVolume;
if (mode === "meeting") {
funcSetVolume = volumeFactory(10);
} else {
funcSetVolume = volumeFactory(50);
}
alert(funcSetVolume(1)); // min has a clousure, so min=10 and min + 1 = 11
闭包是一种特殊的组合对象:一个函数,以及创建该函数的环境。环境包括在创建闭包时作用域内的任何局部变量。我没有提到,但前面讨论的命名空间、库和模块都是闭包的例子。可以在此处看到一个更有趣的闭包示例。
JavaScript 是一种 OO 语言吗?
开发人员熟悉继承的概念,即“派生类是一种基类”。他们从学校教育和计算机科学教科书中了解到这一点。因此,他们习惯于认为类 Car
继承 Vehicle {}
,而类 Truck
继承 Vehicle {}
是Vehicle
作为Car
和Truck
基类的父子或是一种关系的快速 OO 实现。
当这些开发人员尝试在 JavaScript 中进行 OO 时,他们会意识到除了父或子之外,还有朋友。朋友之间的关系是,如果你不知道如何自己做某事,就向其他朋友寻求帮助!所以,从某种意义上说,JavaScript 的基于原型的链式调用表明父对象实际上是其子对象的朋友:)但这只是一个观点。
开发人员擅长思考和建模父子关系,他们这样做的原因有两个:A) 更好地理解代码,B) 重用代码。您已经看到 JavaScript 以一种略有不同的方式提供了多种方法来完成这两件事。一旦您掌握了这门语言,您就会爱上它。
我只是粗略地介绍了 JavaScript 的一些功能。希望在后续的文章中能看到更多它的强大之处。
下一步
起初,我曾考虑在这部分内容中涵盖 Backbone、Angular、Knockout 等 JavaScript 框架,但似乎最好将它们留待下次讨论,并使这一部分专门介绍 JavaScript。我们将在下次深入研究这些流行的框架,并看看它们解决了什么问题或带来了什么挑战。