65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript 摘要

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (124投票s)

2015年7月2日

CPOL

30分钟阅读

viewsIcon

182541

简要总结了 JavaScript 的主要特性,包括对不同类型的 JS 对象、基本数据结构、函数作为一等公民以及实现类的代码模式的讨论。“20 多年的编程经验,这是任何语言的最佳概述!”

20 多年的编程经验,这是任何语言的最佳概述!
[会员 11032252 的评论]

目录

  1. 引言
  2. 类型和数据字面量
  3. 变量作用域
  4. 严格模式
  5. 不同类型的对象
  6. 数组列表
  7. 映射
  8. 四种重要的数据结构类型
  9. 过程和函数
  10. 定义和使用类
  11. LocalStorage API
  12. 延伸阅读

引言

JavaScript 是一种动态的、函数式的、面向对象的编程语言,可用于

  1. 通过以下方式丰富网页:

    • 生成特定于浏览器的 HTML 内容或 CSS 样式,

    • 插入动态 HTML 内容,

    • 产生特殊的视听效果(动画)。

  2. 通过以下方式丰富 Web 用户界面:

    • 实现高级用户界面组件,

    • 在客户端验证用户输入,

    • 自动预填充某些表单字段。

  3. 实现一个具有本地或远程数据存储的前端 Web 应用程序,如书籍《使用纯 JavaScript 构建前端 Web 应用》中所述。

  4. 为分布式 Web 应用程序实现一个前端组件,该组件具有由后端组件管理的远程数据存储,后端组件是传统上用 PHP、Java 或 C# 等服务器端语言编写的服务器端程序,但如今也可以使用 NodeJS 的 JavaScript 编写。

  5. 实现一个完整的分布式 Web 应用程序,其中前端和后端组件都是 JavaScript 程序。

目前 Web 浏览器支持的 JavaScript 版本称为“ECMAScript 5.1”,或简称“ES5”,但接下来的两个版本,称为“ES6”和“ES7”(或“ES 2015”和“ES 2016”,因为新版本计划每年发布一次),具有大量新增功能和改进的语法,即将到来(并且已经得到当前浏览器和后端 JS 环境的部分支持)。

本文档,也可作为 PDF 文件获取,摘自书籍《使用纯 JavaScript 构建前端 Web 应用》,该书提供开放访问的在线电子书。它试图将 Douglas Crockford经典 JavaScript 摘要的所有重要观点考虑在内。

类型和数据字面量

JavaScript 有三种原始数据类型:stringnumberboolean,我们可以通过 typeof(v) 来测试变量 v 是否持有此类类型的值,例如 typeof(v)==="number"

有五种基本引用类型:ObjectArrayFunctionDateRegExp。数组、函数、日期和正则表达式是特殊类型的对象,但从概念上讲,日期和正则表达式是原始数据值,碰巧以包装对象的形式实现。

变量、数组元素、函数参数和返回值的类型未声明,并且通常不会被 JavaScript 引擎检查。类型转换(强制类型转换)会自动执行。

变量的值可以是

  1. 数据值:字符串、数字或布尔值;

  2. 对象引用:引用普通对象、数组、函数、日期或正则表达式;

  3. 特殊数据值 null,通常用作初始化对象变量的默认值;

  4. 特殊数据值 undefined,它是已声明但未初始化的所有变量的隐式初始值。

字符串是 Unicode 字符序列。字符串字面量,如 "Hello world!"、'A3F0' 或空字符串 "",用单引号或双引号括起来。两个字符串表达式可以用 + 运算符连接,并用三等号运算符检查是否相等

if (firstName + lastName === "James Bond") ...

可以通过将 length 属性应用于字符串来获取字符串的字符数

console.log( "Hello world!".length);  // 12

所有数字数据值均以 64 位浮点格式表示,并带有一个可选的指数(如数字数据字面量 3.1e10)。整数和浮点数之间没有显式的类型区分。如果数字表达式无法求值为数字,则其值设置为 NaN(“非数字”),可以使用内置谓词 isNaN(expr) 进行测试。

不幸的是,一个用于测试数字是否为整数的内置函数 Number.isInteger 仅在 ES6 中添加,因此对于在尚不支持它的浏览器中使用它,需要一个polyfill。为了确保数值是整数,或者表示数字的字符串被转换为整数,必须应用预定义函数 parseInt。类似地,可以通过 parseFloat 将表示小数的字符串转换为此数字。要将数字 n 转换为字符串,最好的方法是使用 String(n)

与 Java 一样,有两种预定义的布尔数据字面量:truefalse,布尔运算符符号是感叹号 ! 表示 NOT,双安培符 && 表示 AND,双竖线 || 表示 OR。当非布尔值用在条件中,或作为布尔表达式的操作数时,它会根据以下规则隐式转换为布尔值。空字符串、(数字)数据字面量 0、以及 undefinednull,映射到 false,而所有其他值映射到 true。这种转换可以通过双重否定操作 !! 显式执行。

对于相等性和不等性测试,始终使用三等号 ===!== 而不是双等号 ==!=。否则,例如,数字 2 将与字符串 "2" 相同,因为条件 (2 == "2") 在 JavaScript 中会评估为 true。

分配空数组字面量,例如 var a = [],等同于在不带参数的情况下调用 Array() 构造函数,如 var a = new Array()

分配空对象字面量,例如 var o = {},等同于在不带参数的情况下调用 Object() 构造函数,如 var o = new Object()。但请注意,空对象字面量 {} 并非真正为空,因为它包含从Object.prototype 继承的属性和方法。因此,一个真正空的对象必须使用 null 作为原型创建,如 var o = Object.create( null)

表 1:类型测试和转换
类型 测试 x 的类型 转换为字符串 将字符串转换为类型
字符串 typeof(x)==="string" 不适用 不适用
布尔值 typeof(x)==="boolean" String(x) Boolean(y)
(浮点)数字 typeof(x)==="number" String(x) parseFloat(y)
整数 Number.isInteger(x)*) String(x) parseInt(y)
对象 typeof(x)==="object" x.toString()JSON.stringify(x) JSON.parse(y)
数组 Array.isArray(x) x.toString()JSON.stringify(x) y.split()JSON.parse(y)
函数 typeof(x)==="function" x.toString() new Function(y)
日期 x instanceof Date x.toISOString() new Date(y)
RegExp x instanceof RegExp x.toString() new RegExp(y)

*) 可能需要polyfill

变量作用域

在当前版本的 JavaScript ES5 中,变量只有两种作用域:全局作用域(以 window 作为上下文对象)和函数作用域,但**没有块作用域**。因此,在代码块内声明变量会造成混淆,应避免。例如,尽管这是一种常用模式,甚至被经验丰富的 JavaScript 程序员使用,但在 for 循环中声明循环的计数器变量是一种陷阱,如

function foo() {
  for (var i=0; i < 10; i++) {
    ...  // do something with i
  }
}

相反,正如 JavaScript 解释此代码的方式一样,我们应该写成

function foo() {
  var i=0;
  for (i=0; i < 10; i++) {
    ...  // do something with i
  }
}

所有变量都应在函数开头声明。只有在下一版本 JavaScript ES6 中,才会通过关键字 let 的新变量声明形式来支持块作用域。

严格模式

从 ES5 开始,我们可以使用严格模式来获得更多的运行时错误检查。例如,在严格模式下,所有变量都必须声明。对未声明变量的赋值会抛出异常。

我们可以通过在 JavaScript 文件或 <script> 元素内的第一行输入以下语句来开启严格模式

'use strict';

通常建议使用严格模式,除非你的代码依赖于与严格模式不兼容的库。

不同类型的对象

JS 对象本质上是一组名称-值对,也称为,其中名称可以是属性名称、函数名称或 (hash) map 的。对象可以以任意方式创建,使用 JavaScript 的对象字面量表示法 (JSON),而无需实例化类

var person1 = { lastName:"Smith", firstName:"Tom"};
var o1 = Object.create( null);  // an empty object with no slots

JS 对象与经典的 OO/UML 对象不同。特别是,它们**无需实例化类**。它们可以拥有自己的(实例级)方法,形式为方法槽,因此它们不仅拥有(普通)**属性槽**,还拥有**方法槽**。此外,它们也可能拥有**键值槽**。因此,它们可能有三种不同类型的槽,而经典对象只有属性槽。

每当槽中的名称是可接受的 JavaScript 标识符时,该槽可以是属性槽方法槽键值槽。否则,如果名称是其他类型的字符串(特别是在包含任何空格时),则该槽代表一个键值槽,它是 map 元素,如下所述。

属性槽中的名称可以表示

  1. 数据值属性,在这种情况下,值为数据值,或更通用地说,是数据值表达式

  2. 对象值属性,在这种情况下,值为对象引用,或更通用地说,是对象表达式

方法槽中的名称表示JS 函数(最好称为方法),其值为JS 函数定义表达式

可以通过两种方式访问对象属性

  1. 使用点表示法(如 C++/Java 中)

    person1.lastName = "Smith"
  2. 使用 map 表示法

    person1["lastName"] = "Smith"

JS 对象可以以许多不同的方式用于不同目的。以下是 JS 对象使用的五种不同方式或可能的含义:

  1. 记录是一组属性槽,例如:

    var myRecord = {firstName:"Tom", lastName:"Smith", age:26}
  2. Map(在其他语言中也称为“关联数组”、“字典”、“哈希映射”或“哈希表”)支持基于查找,例如:

    var numeral2number = {"one":"1", "two":"2", "three":"3"}

    它将值“1”与键“one”关联,“2”与“two”关联,依此类推。键不必是有效的 JavaScript 标识符,可以是任何字符串(例如,可能包含空格)。

  3. 未类型对象不实例化类。它可能具有属性槽和函数槽,例如:

    var person1 = { 
      lastName: "Smith", 
      firstName: "Tom",
      getFullName: function () {
        return this.firstName +" "+ this.lastName; 
      }  
    };
  4. 命名空间可以定义为通过全局对象变量引用的未类型对象的形式,其名称表示命名空间前缀。例如,以下对象变量提供了基于模型-视图-控制器 (MVC) 架构范例的应用程序的主要命名空间,其中我们有三个子命名空间对应于 MVC 应用程序的三个部分:

    var myApp = { model:{}, view:{}, ctrl:{} };

    通过使用立即调用的 JS 函数表达式,可以获得更高级的命名空间机制,如下文所述。

  5. 已类型对象实例化一个类,该类由 JavaScript 构造函数或工厂对象定义。请参阅下面的“定义和使用类”部分。

数组列表

JS 数组实际上代表了数组列表的逻辑数据结构,它是一个列表,其中每个列表项都可以通过索引号(如数组的元素)访问。使用“数组”一词而不说“JavaScript 数组”会造成术语上的歧义。但为简单起见,我们有时会只说“数组”而不是“JavaScript 数组”。

变量可以用 JavaScript数组字面量初始化

var a = [1,2,3];

由于它们是数组列表,JS 数组可以动态增长:可以使用大于数组长度的索引。例如,在上面的数组变量初始化之后,变量 a 中保存的数组长度为 3,但我们仍然可以赋值第五个数组元素,如下所示:

a[4] = 7;

数组 a 的内容通过标准的for 循环处理,计数器变量从第一个数组索引 0 递增到最后一个数组索引,即 a.length-1

for (i=0; i < a.length; i++) { ...}

由于数组是特殊类型的对象,我们有时需要一种方法来确定变量是否表示数组。我们可以使用 Array.isArray( a) 来测试变量 a 是否表示数组。

要向数组**添加**新元素,我们使用 push 操作将其附加到数组,如下所示:

a.push( newElement);

要从数组 a 中**删除**位置 i 的元素,我们使用预定义的数组方法 splice,如下所示:

a.splice( i, 1);

要**搜索**数组 a 中的值 v,我们可以使用预定义的数组方法 indexOf,如果找到则返回位置,否则返回 -1,如下所示:

if (a.indexOf(v) > -1)  ...

要**遍历**数组 a,我们有两个选项:for 循环或数组方法 forEach。无论哪种情况,我们都可以使用 for 循环,如下例所示:

var i=0;
for (i=0; i < a.length; i++) {
  console.log( a[i]);
}

如果性能不重要,即如果 a 足够小(例如,它不包含超过几百个元素),我们可以使用预定义的数组方法 forEach,如下例所示,其中参数 elem 迭代地取 a 数组的每个元素作为其值:

a.forEach( function (elem) {
  console.log( elem);
}) 

要**克隆**数组 a,我们可以使用数组函数 slice,如下所示:

var clone = a.slice(0);

映射

(也称为“哈希映射”或“关联数组”) 提供从键到其关联值的映射。JS Map 的键是字符串字面量,可能包含空格,如下所示:

var myTranslation = { 
    "my house": "mein Haus", 
    "my boat": "mein Boot", 
    "my horse": "mein Pferd"
}

Map 使用一种特殊的循环来处理,我们使用预定义的函数 Object.keys(m) 遍历 Map 的所有键,该函数返回 Map m 的所有键的数组。例如:

var i=0, key="", keys=[];
keys = Object.keys( myTranslation);
for (i=0; i < keys.length; i++) {
  key = keys[i];
  alert('The translation of '+ key +' is '+ myTranslation[key]);
}

要向 Map**添加**新条目,我们只需将新值与其键关联,如下所示:

myTranslation["my car"] = "mein Auto";

要从 Map 中**删除**条目,我们可以使用预定义的 delete 运算符,如下所示:

delete myTranslation["my boat"];

要**搜索** Map 是否包含特定键值的条目,例如,测试翻译 Map 是否包含“my bike”的条目,我们可以检查以下内容:

if ("my bike" in myTranslation)  ...

要**遍历** Map m,我们首先使用预定义的 Object.keys 方法将其转换为其键的数组,然后我们可以使用 for 循环或 forEach 方法。以下示例展示了如何使用 for 循环:

var i=0, key="", keys=[];
keys = Object.keys( m);
for (i=0; i < keys.length; i++) {
  key = keys[i];
  console.log( m[key]);
}

同样,如果 m 足够小,我们可以使用 forEach 方法,如下例所示:

Object.keys( m).forEach( function (key) {
  console.log( m[key]);
}) 

请注意,使用 forEach 方法更简洁。

要**克隆** Map m,我们可以使用 JSON.stringify 和 JSON.parse 的组合。我们首先使用 JSON.stringify 将 m 序列化为字符串表示形式,然后使用 JSON.parse 将字符串表示形式反序列化为 Map 对象:

var clone = JSON.parse( JSON.stringify( m))

请注意,如果 Map 只包含简单数据值或(可能嵌套的)包含简单数据值的数组/Map,此方法效果很好。在其他情况下,例如,如果 Map 包含 Date 对象,我们就必须编写自己的克隆方法。

四种重要的数据结构类型

总而言之,JavaScript 支持的四种重要数据结构类型是:

  1. 数组列表,如 ["one","two","three"],它们是称为“数组”的特殊 JS 对象,但由于它们是动态的,因此它们更像是 Java 编程语言中定义的数组列表

  2. 记录,它们是特殊 JS 对象,如 {firstName:"Tom", lastName:"Smith"},如上所述,

  3. Map,它们也是特殊 JS 对象,如 {"one":1, "two":2, "three":3},如上所述,

  4. 实体表,例如下面的表 2,它们是特殊 Map,其中值是具有标准 ID(或主键)槽的实体记录,使得 Map 的键是这些实体记录的标准 ID。

表 2:表示书籍集合的实体表
006251587X { isbn:"006251587X," title:"Weaving the Web", year:2000 }
0465026567 { isbn:"0465026567," title:"Gödel, Escher, Bach", year:1999 }
0465030793 { isbn:"0465030793," title:"I Am A Strange Loop", year:2008 }

请注意,我们在 JavaScript 中区分 Map、记录和实体表纯粹是概念上的区分,JavaScript 执行语义不支持。对于 JavaScript 引擎来说,{firstName:"Tom", lastName:"Smith"}{"one":1, "two":2, "three":3} 都只是对象,它会将 Map 样式的对象 {"one":1, "two":2, "three":3} 的解释方式与记录样式的对象 {one:1, two:2, three:3} 相同。但从概念上讲,{firstName:"Tom", lastName:"Smith"} 是一个记录,因为 firstNamelastName 用于表示属性(或“字段”),而 {"one":1, "two":2, "three":3} 是一个 Map,因为 "one""two" 不用于表示属性/字段,而仅仅是作为 Map 中的键的任意字符串值。

做出这些概念上的区分有助于程序的逻辑设计,并将它们映射到句法区别,即使它们没有被以不同的方式解释,也有助于更好地理解代码的预期计算含义,从而提高其可读性。

过程和函数

如图 1 所示,JS 函数是特殊的 JS 对象,具有可选的 name 属性和提供参数数量的 length 属性。如果变量 v 引用一个函数,可以用以下方式测试:

if (typeof( v) === "function") {...}

由于 JS 函数是 JS 对象,它们可以存储在变量中、作为参数传递给函数、由函数返回、拥有属性并且可以动态更改。因此,函数是一等公民,JavaScript 可以被视为一种函数式编程语言。

函数定义的一般形式是将函数表达式分配给一个变量

var myFunction = function theNameOfMyFunction () {...}

其中 theNameOfMyFunction 是可选的。当省略时,该函数是**匿名**的。无论如何,函数是通过引用该函数的变量来调用的。在上述情况下,这意味着该函数是通过 myFunction() 调用的,而不是通过 theNameOfMyFunction()

匿名函数表达式在其他编程语言中称为lambda 表达式(或简称lambda)。

作为一个匿名函数表达式作为参数传递给另一个(高阶)函数的调用的例子,我们可以采用一个比较函数传递给预定义的函数 sort 来对数组列表的元素进行排序。这样的比较函数必须返回一个负数,如果它的第一个参数被认为小于第二个参数,则返回 0,如果两个参数等级相同,则返回一个正数,如果第二个参数被认为小于第一个参数。在以下示例中,我们按字典顺序对二维数字列表的列表进行排序:

var list = [[1,2],[1,3],[1,1],[2,1]]; 
list.sort( function (x,y) { 
  return ((x[0] === y[0]) ? x[1]-y[1] : x[0]-y[0]);
});

函数声明具有以下形式:

function theNameOfMyFunction () {...}

它等同于以下命名函数定义:

var theNameOfMyFunction = function theNameOfMyFunction () {...}

也就是说,它创建了一个名为 theNameOfMyFunction 的函数和一个引用该函数的变量 theNameOfMyFunction

JS 函数可以有**内部函数**。**闭包**机制允许 JS 函数使用其外部作用域中的变量(不包括 this),并且在闭包中创建的函数会记住其创建的环境。在以下示例中,无需将外部作用域变量 result 作为参数传递给内部函数,因为它随时可用:

var sum = function (numbers) {
  var result = 0;
  numbers.forEach( function (n) {
      result += n;
  });
  return result;
};
console.log( sum([1,2,3,4]));

当执行方法/函数时,我们可以通过使用内置的 arguments 对象在其主体中访问其参数,该对象是“类数组”的,因为它具有索引元素和 length 属性,我们可以使用常规的 for 循环对其进行迭代,但由于它不是 Array 的实例,因此 JS 数组方法(如 forEach 循环方法)不能应用于它。arguments 对象包含传递给方法的每个参数的一个元素。这允许在不带参数定义方法,并用**任意数量的参数**调用它,如下所示:

var sum = function () {
  var result = 0, i=0;
  for (i=0; i < arguments.length; i++) {
    result = result + arguments[i];
  }
  return result;
};
console.log( sum(0,1,1,2,3,5,8));  // 20

在构造函数原型上定义的方法,可以由该构造函数创建的所有对象调用,例如 Array.prototype.forEach,其中 Array 表示构造函数,必须使用类的实例作为**上下文对象**通过 this 变量引用来调用(另请参阅下一节关于类)。在以下示例中,数组 numbers 是调用 forEach 时的上下文对象:

var numbers = [1,2,3];  // create an instance of Array
numbers.forEach( function (n) {
  console.log( n);
});

每当要使用普通参数而不是上下文对象调用此类原型方法时,可以通过**JS 函数 call 方法**来实现,该方法将调用方法所在的对象作为其第一个参数,后跟要调用方法的参数。例如,我们可以通过以下方式将 forEach 循环方法应用于类数组对象 arguments

var sum = function () {
  var result = 0;
  Array.prototype.forEach.call( arguments, function (n) {
    result = result + n;
  });
  return result;
};

Function.prototype.call 方法的一个变体,将要调用方法的所有参数作为单个数组参数,是 Function.prototype.apply

每当要不带上下文对象调用原型中定义的方法,或者在不带其上下文对象调用对象上下文中定义的方法时,我们可以通过JS 函数 bind 方法Function.prototype.bind)将其 this 变量绑定到给定对象。这允许创建调用方法的快捷方式,如 var querySel = document.querySelector.bind( document),这允许使用 querySel 而不是 document.querySelector

可以使用立即调用的 JS 函数表达式选项来获得优于使用纯命名空间对象的命名空间机制,因为它可以控制哪些变量和方法是全局公开的,哪些不是。此机制也是 JS 模块概念的基础。在以下示例中,我们为应用程序的模型代码部分定义了一个命名空间,该命名空间以构造函数的形式公开了一些变量和模型类:

myApp.model = function () {
  var appName = "My app's name";
  var someNonExposedVariable = ...;
  function ModelClass1 () {...}
  function ModelClass2 () {...}
  function someNonExposedMethod (...) {...}
  return {
    appName: appName,
    ModelClass1: ModelClass1,
    ModelClass2: ModelClass2
  }
}();  // immediately invoked

这种模式已在 WebPlatform.org 文章JavaScript 最佳实践中提出。

定义和使用类

的概念在面向对象编程中是基础。对象实例化(或被分类)一个类。类定义了使用它的对象的属性和方法(作为蓝图)。拥有类概念对于在现代软件应用程序中实现模型类形式的数据模型至关重要,尤其是在模型-视图-控制器 (MVC) 架构中。然而,在经典的 OO 语言(如 Java)中,类及其继承/扩展机制被过度使用,在这些语言中,所有变量和过程都必须在类的上下文中定义,因此,类不仅用于实现对象类型(或模型类),还用作这些语言中许多其他目的的容器。JavaScript 中并非如此,我们可以自由地仅使用类来实现对象类型,同时将方法库保留在命名空间对象中。

任何用于在 JavaScript 中定义类的代码模式都应满足五个要求。首先,(1)它应允许定义类名、一组(实例级)**属性**,最好可以选择将它们保留为“私有”,一组(实例级)**方法**,以及一组类级属性和方法。最好能够使用范围/类型和其他元数据(如约束)来声明属性。还应有两个内省功能:(2)一个**is-instance-of 断言**,可用于检查对象是否是类的直接或非直接实例,以及(3)一个用于检索对象**直接类型**的实例级属性。此外,还有一个用于检索类的直接超类的第三个内省功能是理想的。最后,应有两种继承机制:(4)**属性** **继承**和(5)**方法** **继承**(带方法覆盖)。此外,支持多重继承多重分类,允许对象通过实例化多个角色类来同时扮演多个角色,这是理想的。

JavaScript 中没有显式的类概念。用于在 JavaScript 中定义类的不同代码模式已被提出并在不同的框架中使用。但它们通常不满足上述五个要求。定义类的两种最重要的方法是:

  1. 以**构造函数**的形式,它通过**原型链**实现方法继承,并允许使用 new 运算符创建类的实例。这是 Mozilla 在其JavaScript 指南中推荐的经典方法(并在 ES6 class 语法中实现)。

  2. 以**工厂**对象的形式,它使用预定义的 Object.create 方法来创建类的实例。在这种方法中,必须用另一种机制替换基于构造函数的继承机制。Eric Elliott 曾论证工厂类是 JavaScript 中基于构造函数的类的可行替代方案(事实上,他甚至谴责使用经典继承和基于构造函数的类,把洗澡水和婴儿一起倒掉)。

在构建应用程序时,我们可以根据应用程序的需求使用这两种类型的类。由于我们经常需要定义类层次结构,而不仅仅是单个类,但我们必须确保我们不在此类层次结构中混淆这两种替代方法。虽然工厂类方法(例如mODELcLASSjs 库)有很多优点,如表 3 中所述,但基于构造函数的方法在对象创建性能方面具有优势。

表 3. JS 类代码模式的必需和期望的功能
类功能 基于构造函数的方法 基于工厂的方法 mODELcLASSjs
定义属性和方法
is-instance-of 断言
直接类型属性
类的直接超类属性 可能
属性继承
方法继承
多重继承 可能
多重分类 可能
允许对象池

ES6 中的基于构造函数的类

仅在 ES6 中,引入了一种用户友好的语法来定义基于构造函数的类(使用新关键字 classconstructorstaticextendssuper)。这种新语法允许分三个步骤定义简单的类层次结构。

在**步骤 1.a)** 中,定义了一个基类 Person,该类有两个属性 firstNamelastName,以及一个(实例级)方法 toString 和一个静态(类级)方法 checkLastName

class Person {
  constructor( first, last) {
    this.firstName = first;
    this.lastName = last;
  }
  toString() {
    return this.firstName + " " +
        this.lastName;
  }
  static checkLastName( ln) {
    if (typeof(ln)!=="string" || 
        ln.trim()==="") {
      console.log("Error: " +
          "invalid last name!");
    }
  }
}

在**步骤 1.b)** 中,定义了类级别(“静态”)属性

Person.instances = {};

最后,在**步骤 2** 中,定义了一个子类,其中包含可能覆盖相应超类方法的附加属性和方法。

class Student extends Person {
  constructor( first, last, studNo) {
    super.constructor( first, last);
    this.studNo = studNo; 
  }
  // method overrides superclass method
  toString() {
    return super.toString() + "(" +
        this.studNo +")";
  }
}

ES5 中的基于构造函数的类

在 ES5 中,我们可以以构造函数的形式定义一个基于构造函数的类层次结构,遵循 Mozilla 在其JavaScript 指南中推荐的代码模式。此代码模式需要七个步骤来定义简单的类层次结构。由于这种复杂的模式相当繁琐,使用像cLASSjs 这样的库来简化基于构造函数的类和类层次结构的定义可能更可取。

步骤 1.a) 首先定义构造函数,该函数在新对象创建时通过将属性分配给构造函数参数的值来隐式定义类的属性。

function Person( first, last) {
  this.firstName = first; 
  this.lastName = last; 
}

请注意,在构造函数中,特殊变量 this 指代创建构造函数时创建的新对象。

步骤 1.b) 接下来,将类的实例级方法定义为构造函数prototype属性所引用的对象的方法槽

Person.prototype.toString = function () {
  return this.firstName + " " + this.lastName;
}

步骤 1.c)类级别(“静态”)方法可以定义为构造函数本身的方法槽(请记住,由于 JS 函数是对象,它们可以拥有槽),如下所示:

Person.checkLastName = function (ln) {
  if (typeof(ln)!=="string" || ln.trim()==="") {
    console.log("Error: invalid last name!");
  }
}

步骤 1.d) 最后,将类级别(“静态”)属性定义为构造函数的方法槽:

Person.instances = {};

步骤 2.a):定义一个具有附加属性的子类。

function Student( first, last, studNo) {
  // invoke superclass constructor
  Person.call( this, first, last);
  // define and assign additional properties
  this.studNo = studNo;  
}

通过为新对象(由 this 引用)调用超类型构造函数 Person.call( this, ...),并将其作为子类型 Student 的实例,我们实现了子类型实例也创建了在超类型构造函数中创建的属性槽(firstNamelastName),以及给定类层次结构中的整个超类型链。通过这种方式,我们建立了一个**属性继承**机制,该机制确保对象创建时定义的自有属性包括超类型构造函数定义的自有属性。

在**步骤 2b)** 中,我们建立了一个通过构造函数的 prototype 属性实现**方法继承**的机制。我们将从超类型的 prototype 对象创建的新对象分配给子类构造函数的 prototype 属性,并调整原型的构造函数属性:

// Student inherits from Person
Student.prototype = Object.create( 
    Person.prototype);
// adjust the subtype's constructor property
Student.prototype.constructor = Student;

通过 Object.create( Person.prototype),我们创建一个以 Person.prototype 作为其原型且没有任何自有属性槽的新对象。通过将此对象分配给子类构造函数的 prototype 属性,我们实现了在子类实例中也可用在超类中定义和继承的方法。这种原型链机制负责方法继承。请注意,将 Student.prototype 设置为 Object.create( Person.prototype) 比设置为 new Person() 更好,后者是在 ES5 出现之前的实现相同目的的方式。

步骤 2c):定义一个覆盖超类方法的子类方法。

Student.prototype.toString = function () {
  return Person.prototype.toString.call( this) +
      "(" + this.studNo + ")";
};

通过对构造函数应用 new 运算符并为构造函数参数提供合适的参数来创建基于构造函数的类的实例:

var pers1 = new Person("Tom","Smith");

使用“点表示法”在类型为 Person 的对象 pers1 上调用方法 toString

alert("The full name of the person are: " + 
     pers1.toString());

当使用 o = new C(...) 创建对象 o 时,其中 C 引用一个名为“C”的函数时,可以通过内省表达式 o.constructor.name 检索 o 的类型(或类)名称,该表达式返回“C”。但是,此表达式中使用的 Function::name 属性除了 Internet Explorer 11 及更早版本外,都被所有浏览器支持。

JavaScript 的原型特性

在 JavaScript 中,**原型**是一个具有方法槽(有时也具有属性槽)的对象,可以通过 JavaScript 的方法/属性槽查找机制被其他对象继承。该机制遵循通过(在 ES5 中仍非官方的)内置引用属性 __proto__(带有双下划线前缀和后缀)定义的**原型链**来查找方法或属性。如图 1 所示,每个构造函数都通过其引用属性 prototype 的值引用一个原型。当使用 new 创建新对象时,其 __proto__ 属性设置为构造函数的 prototype。例如,在用 f = new Foo() 创建新对象后,它保持 Object.getPrototypeOf(f)(等于 f.__proto__)等于 Foo.prototype。因此,对 Foo.prototype 的槽的更改会影响所有用 new Foo() 创建的对象。虽然每个对象都有一个 __proto__ 属性槽(除了 Object),但只有用 new 构造的对象才有一个 constructor 属性槽。

图 1. 内置 JavaScript 类 ObjectFunction

基于工厂的类

在这种方法中,我们定义一个 JS 对象 Person(实际上代表一个类),其中包含一个特殊方法 create,该方法调用预定义的 Object.create 方法来创建 Person 类型的对象:

var Person = {
  name: "Person",
  properties: {
    firstName: {range:"NonEmptyString", label:"First name", 
        writable: true, enumerable: true},
    lastName: {range:"NonEmptyString", label:"Last name", 
        writable: true, enumerable: true}
  },
  methods: {
    getFullName: function () {
      return this.firstName +" "+ this.lastName; 
    }
  },
  create: function (slots) {
    // create object
    var obj = Object.create( this.methods, this.properties);
    // add special property for *direct type* of object
    Object.defineProperty( obj, "type", 
        {value: this, writable: false, enumerable: true});
    // initialize object
    Object.keys( slots).forEach( function (prop) {
      if (prop in this.properties) obj[prop] = slots[prop];
    })
    return obj;
  }
};

请注意,JS 对象 Person 实际上代表一个基于工厂的类。通过调用其 create 方法来创建此类工厂类实例:

var pers1 = Person.create( {firstName:"Tom", lastName:"Smith"});

通过“点表示法”在类型为 Person 的对象 pers1 上调用方法 getFullName,与基于构造函数的方法类似:

alert("The full name of the person are: " + pers1.getFullName());

请注意,使用 Object.create 创建的每个对象属性声明都必须包含“描述符”writable: trueenumerable: true,如上面 Person 对象定义中的第 5 行和第 7 行所示。

在一般方法中,如mODELcLASSjs 库用于模型驱动开发中,我们不会在每个类定义中重复定义 create 方法,而是有一个通用的构造函数来定义基于工厂的类。这样的工厂类构造函数,如 mODELcLASS,还将通过合并自有属性和方法与超类的属性和方法来提供**继承**机制。

JavaScript 作为面向对象语言

JavaScript 是面向对象的,但方式与 Java 和 C++ 等经典 OO 编程语言不同。JavaScript 中没有显式的概念。相反,类必须以特殊对象的形式定义:要么是构造函数,要么是工厂对象。

然而,对象也可以在不实例化类的情况下创建,在这种情况下它们是未类型的,并且可以独立于任何类定义为特定对象定义属性和方法。在运行时,属性和方法可以添加到任何对象和类,或从中删除。JavaScript 的这种动态性允许强大的元编程形式,例如定义自己的类或枚举概念。

LocalStorage API

对于前端应用程序,我们需要能够在前端设备上持久存储数据。现代 Web 浏览器为此目的提供了两种技术:一种更简单的方式称为Local Storage,另一种更强大的方式称为IndexDB

每个浏览器和每个源(由协议和域名组合定义)都会创建一个 Local Storage 数据库。例如,http://example.comhttp://www.example.com 是不同的源,因为它们的域名不同,而 http://www.example.comhttps://www.example.com 是不同的源,因为它们的协议不同(HTTP 与 HTTPS)。

由浏览器管理并与应用程序(通过其源)关联的 Local Storage 数据库作为内置 JavaScript 对象 localStorage 公开,其中包含 getItemsetItemremoveItemclear 方法。但是,与其调用 getItemsetItem,不如更方便地将 localStorage 作为 Map 处理,通过将值赋给键来写入,例如 localStorage["id"] = 2901465,并通过读取 Map 来检索数据,例如 var id = localStorage["id"]

以下示例展示了如何创建实体表并将其序列化保存到 Local Storage:

var people = {};
people["2901465"] = {id: 2901465, name:"Tom"};
people["3305579"] = {id: 3305579, name:"Su"};
people["6492003"] = {id: 6492003, name:"Pete"};
try {
  localStorage["personTable"] = JSON.stringify( people);
} catch (e) {
  alert("Error when writing to Local Storage\n" + e);
}

请注意,我们使用了预定义的 JSON.stringify 方法将实体表 people 序列化为字符串,该字符串被赋给 localStorage 键“personTable”的值。我们可以通过预定义的反序列化方法 JSON.parse 来检索该表,如下所示:

var people = {};
try {
  people = JSON.parse( localStorage["personTable"]);
} catch (e) {
  alert("Error when reading from Local Storage\n" + e);        
}

延伸阅读

关于 JavaScript 的优秀开放访问书籍有:

历史

  • 2016 年 7 月 25 日:改进了“槽”的定义/使用顺序,添加了目录。
  • 2016 年 5 月 4 日:添加了关于 localStorage API 的新部分。
  • 2016 年 3 月 7 日:改进了数据结构解释,在关于类的部分添加了主题“方法覆盖”、超链接和附加子标题。
  • 2016 年 1 月 15 日:添加了新超链接;改进了关于类的部分的比较表和解释。
  • 2015 年 11 月 9 日:添加了关于字符串数据类型的段落;在关于 JS 函数的部分添加了关于“arguments”、call/apply 和 bind 的段落;添加了关于 ES6 类的侧边栏,改进了对基于构造函数的类的讨论。
  • 2015 年 10 月 21 日:添加了 Date 和 RegExp,添加了表 1“类型测试和转换”,纠正了链接。
  • 2015 年 9 月 20 日:添加了 ES 2015+2016 提示、parseFloat、Function::name、Function::length、实体表示例、关于克隆 Map 的段落。
  • 2015 年 8 月 31 日:添加了类代码模式的要求和关于基于构造函数的类的子类型解释,以及图示。
  • 2015 年 7 月 16 日:进一步解释了 JS 中类的定义和使用。
  • 2015 年 7 月 7 日:添加了 1) 指向 Crockford 经典调查的链接,2) 关于 isInteger 和数字到字符串转换的说明。3) 指向 CP 关于 mODELcLASSjs 的文章的链接。
  • 2015 年 7 月 2 日:第一个版本
  • 2016 年 1 月 15 日:添加了新超链接;改进了关于类的部分的比较表和解释。
  • 2015 年 11 月 9 日:添加了关于字符串数据类型的段落;在关于 JS 函数的部分添加了关于“arguments”、call/apply 和 bind 的段落;添加了关于 ES6 类的侧边栏,改进了对基于构造函数的类的讨论。
  • 2015 年 10 月 21 日:添加了 Date 和 RegExp,添加了表 1“类型测试和转换”,纠正了链接。
  • 2015 年 9 月 20 日:添加了 ES 2015+2016 提示、parseFloat、Function::name、Function::length、实体表示例、关于克隆 Map 的段落。
  • 2015 年 8 月 31 日:添加了类代码模式的要求和关于基于构造函数的类的子类型解释,以及图示。
  • 2015 年 7 月 16 日:进一步解释了 JS 中类的定义和使用。
  • 2015 年 7 月 7 日:添加了 1) 指向 Crockford 经典调查的链接,2) 关于 isInteger 和数字到字符串转换的说明。3) 指向 CP 关于 mODELcLASSjs 的文章的链接。
  • 2015 年 7 月 2 日:第一个版本
© . All rights reserved.