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

HTML5、JavaScript、Knockout、JQuery,为 Silverlight/WPF/C# 成瘾者提供的恢复指南。第 1 部分 - JavaScript 和 DOM。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (86投票s)

2012年9月11日

CPOL

26分钟阅读

viewsIcon

186043

downloadIcon

1281

适用于 WPF/Silverlight/C# 开发人员的 JavaScript/DOM 基础知识

介绍  

作为一名顾问,我对技术最新趋势非常敏感。

几年前,我遇到了 WPF 并爱上了它。WPF 引入了全新的概念,使开发人员能够在 UI 应用程序的可视设计和底层非可视实现之间实现几乎完全的分离。同时,WPF 为可视化开发提供了强大的功能。Silverlight 将 WPF 的强大功能带到了多个平台。

然而,最近的发展表明,微软对 WPF 和 Silverlight 均不看好,转而青睐 HTML5/JavaScript。这可能是微软在当前艰难局势下的正确战略。在几年时间内,微软失去了作为最大软件公司的地位,并在互联网浏览器市场失去了优势。其主要原因是微软忽视了智能手机和平板电脑市场,并且进入这些市场为时已晚。作为追随者与作为潮流引领者不同,目前微软必须在自己的游戏中击败所有其他竞争对手,而这个游戏就是 HTML5 和 JavaScript(根据以往的历史,我坚信它将再次成为领导者)。

这些发展促使我投入大量精力学习和使用 HTML5 和 JavaScript。我也在尝试理解如何将我在 WPF/Silverlight 开发中学到的概念应用到 JavaScript 开发中。

本系列文章的目的是让 WPF/Silverlight 开发人员更容易掌握 HTML5/JavaScript 编程,并利用 WPF 和 Silverlight 概念。这些文章假设读者已经对 HTML 和 JavaScript 有一定的基本了解,因此完全不熟悉的人应该从其他地方开始。

我还强烈推荐 pluralsight.com 作为学习 HTML5/JavaScript 的资源。

JavaScript 和 HTML 的优缺点

以下是 JavaScript 和 HTML 的优点
  • 如果你希望你的应用程序能够触达许多不同的平台,包括移动平台(iPhone、Android 和 Windows 8),那么 HTML/JavaScript 是必不可少的,尤其是如果你想避免通过将应用程序发布到 Apple 或 Windows 商店的麻烦。
  • 与我十年前的经验相反,通过利用 JavaScript 库来吸收浏览器差异,人们可以轻松编写真正跨浏览器的 JavaScript 代码,而无需付出太多额外努力。然而,对于 HTML5 和 CSS3 功能来说,情况并非如此。HTML5 和 CSS3 规范尚未最终确定,并且不同浏览器实现了其不同的子集。人们可以访问 caniuse.com 网站,查看哪些浏览器实现了哪些功能。根据该网站,Google Chrome 在 HTML5 和 CSS3 实现方面领先于其他所有浏览器。
  • JavaScript 足够强大,可以实现 WPF 功能,从而实现可视化和非可视化功能之间的分离,即可观察属性和绑定。Knockout.js 库正是这样做的(在某些方面甚至更多)。
  • JavaScript 作为一种弱类型语言,比 C# 更强大——对可以将哪个对象传递给哪个函数没有限制。当然,它的反面是 JavaScript 中更容易出错,并且更难检测。
  • 您可以使用 Ajax 仅以 JSON 格式在服务器和客户端之间发送数据,而不是传输大量的 HTML 代码。
  • 您可以使用 JavaScript 非常快速地进行原型设计。
  • HTML5 提供了强大的视觉功能,可与 WPF 和 Silverlight 相媲美。
  • 随着浏览器日趋成熟,HTML5 功能将越来越多地得到硬件加速,这将大大提高视觉渲染和动画的速度。

JavaScript 脚本具有弱类型语言的所有缺点

  • 其对象的结构不由类定义,实际上,它的对象是在应用程序运行时由函数构建的,因此当你看到例如传递给函数的参数时,你将很难弄清楚它包含什么信息。这使得代码分析相当困难,并且需要更多的文档。良好的文档也应该指定传递给函数的对象的隐含结构。
  • 在 C#、Java 和 C++ 中,架构师和高级开发人员可以通过提供一堆接口并要求团队其他成员使用它们来强制代码的统一性,但在 JavaScript 中不存在这种能力。因此,当团队中有几个经验水平不同的人时,你只能依靠代码审查、严格的代码验收流程、广泛的文档和测试来确保代码质量。
  • 每个 JavaScript 对象都只是一个字段和函数的字典(或映射或袋)。函数通过名称(而不是像强类型语言那样通过偏移量)解析字段。因此,JavaScript 语言比强类型语言慢。
  • 由于对象类型难以推断,IntelliSense 对 JavaScript 的支持不如强类型托管语言好。
  • JavaScript 的核心对象是函数,因此 JavaScript 实际上与函数式语言(如 F# 和 Lisp)有关。像 JQuery.js 或 Knockout.js 这样的通用模块是作为许多不同的函数互相调用而编写的,没有许多中间结果。这使得代码难以分析和调试,尽管 JavaScript 代码也可以用更面向对象的方式编写。

在 MS Visual Studio 中创建简单的 HTML 项目

让我们从在 Microsoft Visual Studio 中创建最简单的 JavaScript 项目开始。虽然我使用的是 VS 2010 SP1,但可以在 VS 2012 中以完全相同的方式创建 HTML/JavaScript 项目。

当您尝试在 Visual Studio 中创建 HTML 或 JavaScript 项目时,您会发现 Microsoft 没有提供任何 HTML 或 JavaScript 模板。创建新项目时,您应该选择“ASP.NET 空 Web 应用程序”项目模板

将项目命名为“HelloWorldJS”,然后单击“确定”按钮创建项目。

通过在解决方案资源管理器中右键单击项目,然后选择“添加”->“新建项”,向项目添加一个名为 HelloWorld.html 的空 HTML 文件。在打开的对话框中,为项类型选择“HTML 页面”,并将文件命名为 HelloWorld.html。

在 HTML "body" 标签内,添加以下代码 <h1>Hello World</h1>。您现在可以运行项目,它将以大写字母显示 "Hello World"。

重要提示:在运行项目之前,请确保 HelloWorld.html 文件在您按下“运行”图标之前在 Visual Studio 中具有键盘焦点。您可以通过在不同的 HTML 文件之间切换键盘焦点来运行它们。

添加 JQuery 引用

JQuery 是一个用于解析 HTML DOM 的跨浏览器 JavaScript 库。它大大简化了开发工作。您可以从 jquery.com 下载最新版本的 JQuery 脚本并手动将其添加到项目中,或者,更简单地,您可以将 JQuery 的引用作为 NuGet 包下载。

在您可以使用 Visual Studio 安装 NuGet 包之前,您需要安装 NuGet 包管理器 VS 扩展。您只需为您的 Visual Studio 安装一次,它就能让您为您需要的任何项目安装 NuGet 包。

要安装 NuGet 包管理器,请在 Visual Studio 中转到“工具”菜单,然后选择“扩展管理器”菜单项。在窗口左侧选择“联机库”选项。在扩展管理器窗口右上角的搜索区域中键入“nuget package manager”,然后单击“NuGet 包管理器”项旁边的“下载”按钮以下载并安装它

安装完成后,请重启 Visual Studio,以便开始使用 NuGet 包管理器。

要为您的项目安装 JQuery,请在解决方案资源管理器中右键单击“引用”,然后选择“管理 NuGet 包”选项(安装 NuGet 包管理器后应该会出现此选项)。在对话框顶部右侧的搜索区域中,键入“jquery”。单击 JQuery 条目中的安装按钮

安装完成后,您可以看到在项目下创建了“Scripts”文件夹,并添加了3个文件:jquery-1.8.1.min.js(针对互联网传输进行了优化)、jquery-1.8.1.js(用于在需要时在您的机器上调试 JQuery)和 jquery-1.8.1-vsdoc.js(用于智能感知)。

现在,您可以将 jquery-1.8.1.min.js 拖放到 HelloWorld.html 文件中的任何位置,例如文件的末尾。这会在您的文件中添加以下行

<script src="Scripts/jquery-1.8.1.min.js" type="text/javascript"></script>

JavaScript 代码可以放置在 HTML 文件中的 <script type="text/javascript"> HTML 标签内,也可以放置在扩展名为 .js 的单独文件中。对于这个简单的例子,我们将把 JavaScript 代码放在 HTML 文件中。

由于我们要在 JavaScript 代码中使用 JQuery,因此我们需要将此代码放在 JQuery 引用之后。

我们的 JavaScript 代码将修改显示的字符串为“Hello Wonderful World”。

由于 JQuery 代码与 DOM 交互,我们需要确保代码只在 DOM 创建后才被调用。为此,我们将整个代码放在 $(document).ready 函数中。

    $(document).ready(
        function () {
            // to make sure that the DOM is created
            // all the relevant code should go here.
        }
    );

由于我们想要引用特定的 HTML DOM 节点,我们需要以某种方式识别该节点。最好的方法是给它一个 id:所以让我们将我们的 HTML 行改为 <h1 id="TheText">Hello World</h1>。JQuery 可以使用“#”选择器通过 id 查找 DOM 元素:$("#TheText");。最后,我们可以使用 text 函数替换标签内的文本。代码如下:

<script type="text/javascript">
    $(document).ready( // "ready" function ensures that 
                       // the functionality within executes after the DOM creation
        function () {
            $("#TheText"). // finds the HTML tag with "TheText" id
                text("Hello Wonderful World"); // changes the text within the tag
        }
    );
</script>

现在运行应用程序将显示“Hello Wonderful World”文本。

在上面的示例中,我们展示了如何使用 JQuery 并编写一个基本的 JavaScript。请注意,与许多其他 JQuery 函数一样,如果未向函数 text 传递任何参数,它将返回标签内的当前文本;如果传递了参数,它将修改标签内的文本。

此示例的代码包含在 HelloWorldJS 项目中。

关于在 Visual Studio 2010 或 2012 中调试 JavaScript 的说明

根据我的经验,Visual Studio 2010 和 2012 的调试仅在您使用 Internet Explorer 运行应用程序时才有效。如果您的应用程序在不同的浏览器中弹出,VS 调试器将无法工作。让您的 Visual Studio 在 Internet Explorer 中启动 HTML/JavaScript 应用程序的最简单方法是将 Internet Explorer 设置为您的默认浏览器。有关实现相同目的而无需更改 PC 默认浏览器的更复杂方法,请参阅 Scott Hansleman 的如何使用 PowerShell 以编程方式更改 Visual Studio 中的默认浏览器并可能戳瞎自己的眼睛

JavaScript 调试器的工作方式与 C# 调试器非常相似:您可以添加断点;一旦程序在断点处停止,您可以检查变量、调用堆栈等。

突出 JavaScript 特性的示例

虽然本文并非旨在作为 JavaScript 参考,但我希望提供几个突出不同 JavaScript 特性的示例。为了显示这些示例的结果,您可以简单地将它们放在上面讨论的 HelloWorldJS 项目的 $(document).ready() 函数中,并将结果插入 text() 函数中,而不是“Hello Wonderful World”字符串。

局部变量和作用域

JavaScript 变量可以在函数内或函数外的全局作用域中声明。最常见的变量声明方式是使用关键字 var,例如 var i = 10;。请注意,花括号不会改变 JavaScript 中的变量作用域
  var i = 52;
  {
     var i = 52;
  }
是定义变量 i 并将其设置为 52 的两种完全等效的方法。以下 JavaScript 将起作用,并且仍然会在屏幕上显示 52
    $(document).ready( 
        function () {

            {
                var i = 52;
            }

            $("#TheText").text(i); 
        }
    );

函数和参数

函数可以命名或匿名。命名函数可以通过名称调用。匿名函数可以在创建后立即调用,或者通过引用调用,就像 C# 中的匿名函数和 lambda 表达式一样。函数可以使用 return 关键字返回一个值,就像 C#、Java 和 C++ 中的函数一样。

这是一个定义和调用名为“MyFunction”的命名函数的示例

  // defining a function
  function MyFunction()
  {
     return 20;
  }
  
  // calling a function
  var i = MyFunction(); // i is set to 20
请注意,即使函数不返回任何值,仍然可以编写 var i = MyNonReturningFunction()。在这种情况下,变量 i 将被设置为“未定义”。

以下是创建匿名函数并立即调用它的示例

  function() {
     var i = 20;
  }();
函数定义后面的括号将确保它立即被调用。

以下是创建时存储匿名函数引用并在稍后调用它的示例

  var f =  function() { // we create an anonymous function and store a reference to it
     return 25;
  };

  ...
  var i = f(); // we call the anonymous function via a reference

函数可以定义参数,例如 function MyFunction(a, b, c),但传递的参数数量不必与函数定义中的参数数量匹配,例如上面定义的 MyFunction 可以不带参数调用:MyFunction()。在这种情况下,未传递的参数在函数内部将被视为“未定义”状态。也可以向函数传递比函数定义中定义的参数更多的参数。在这种情况下,只有前几个参数与函数中定义的参数匹配。访问函数内所有参数的另一种方法是使用 arguments 变量,例如

  function MyFunction()
  {
     var result = "";
     for(var i = 0; i < arguments.length; i++)
     {
         result += arguments[i];
     }

     return result;
  }

  var concatenatedString = MyFunction("Hi ", "World");

上面的函数将连接传递给它的所有参数字符串,因此结果将是“Hi World”。

由于参数列表是灵活的,因此 JavaScript 中没有函数重载,函数在其作用域内应该具有唯一的名称,否则后定义的函数将覆盖具有相同名称的先前定义的函数。

子函数、函数作用域、闭包、全局变量

正如我们上面看到的,我们可以在一个函数中定义另一个函数。例如
  function MyFunction()
  {
     
      var MyChildFunction() // define MyChildFunction
      {
         ...
      };

      ...
      MyChildFunction(); // call MyChildFunction
  }
在函数内部定义的函数称为子函数。

函数定义了一个作用域,即如果你在一个函数内部使用 var 定义一个变量,它将只在该函数本身或该函数的后代中可见,并且在其外部不可见

    function() {
       var i = 20;

       var f = function() {
          // variable i is visible
       }
    }

    // variable i is not visible
  

就像 C# 中的 lambda 一样,函数具有闭包,即函数记录对更高作用域中的变量的引用,而不是值

    function () {
        var i = 10;
        var square = function () {
            i = i * i;
        }

        i = 40;

        var result = square(); // result is 1600, not 100 as one might assume
    }
  

函数可以定义一个全局变量(类似于 C# 中的静态变量)。为了创建这样的变量,应该给变量一个名称,前面没有 var 关键字。在这种情况下,它被分配给表示浏览器窗口的全局 windows 对象,并且可以从任何其他函数访问

    function () {
        globalVariable = 10; // no 'var' in front the variable definition
    }

   // globalVariable is visible outside of the function scope

对象和数组

JavaScript 中有 3 种类型的变量
  • 内置值类型 - 数字、字符串、布尔值等。
  • 对象 - 具有一组字段的引用类型
  • 数组 - 可按索引访问的对象或内置类型的集合。

让我们进一步讨论 Object 类型。本质上,它是一个可按名称访问的名称-值对的 Dictionary/Map/Bag。与 C# 或其他强类型语言不同,没有预定义对象包含内容的类;相反,对象内的字段是在程序运行时动态添加的。可以通过简单地赋值给对象来向对象添加字段

  var myObject = new Object(); // creates a new object
  myObject.someField = 5; // assigns number 5 to 'someField' field within the object.

对象的字段也可以通过名称设置或访问:myObject.someField = 5; 等同于 myObject["someField"] = 5;。此外,可以编写 var myField = myObject["someField"]; 从对象中检索字段。

对象的字段可以是内置类型、其他对象、数组或函数的引用。

现在,让我们简要讨论一下数组。下面是数组创建和操作的示例

  var myArray = new Array();

  myArray[0] = 1; // array resizes as it is assigned
  myArray[10] = "hello world"; // array resizes as it is assigned 
                               // (now myArray.length is 11) and the 
                               // array cells with indices 1 to 9 are not defined.

  myArray.push(2); // adds number 2 to the end of the array, array length is now 12

  var myVar = myArray.pop(); // removes and returns the last array value (number 2) 
                             // myArray.length now is 11
                             // myVar gets value 2

JSON 对象表示法

JSON 是一种紧凑的数据存储和传输方式。它比 XML 紧凑得多。JavaScript 对象和数组可以通过 JSON 表示法创建。这是一个例子
  var student = 
    { 
      firstName:"Joe", 
      lastName:"Doe",
      age: 20,
      friends : [ 
        {firstName:"John", lastName:"Smith"},
        {firstName:"Dan", lastName:"Brown"}
      ]
    };
上面的代码创建了一个学生对象 student,其中 firstName 字段设置为“Joe”,lastName“Doe”,age 20,以及一个朋友数组。

JSON 中的花括号分隔对象边界,方括号分隔数组边界。对象中的多个字段或数组中的单元格用逗号分隔。冒号用于分隔 Object 的名称值对中的名称和值。

JSON 表示法还提供了一种更简洁的初始化空对象和数组的方法

  var myObj = {};
  var myArray = [];   

将面向对象概念映射到 JavaScript

这里我们展示如何将诸如类、接口、虚函数和继承等面向对象概念转换为 JavaScript 语言。

类和构造函数 

C# 和其他面向对象语言中的类指定具有相同“形状”的对象,即具有相似成员但可能数据不同的对象。在 JavaScript 中,我们可以编写一个函数来创建对象,用一些值填充它,然后将其返回给调用者。这个函数将扮演 C# 构造函数的角色,生成具有相似结构的对象。这是一个例子
  function createStudent(firstName, lastName)
  {
     // create an object
     var student = new Object();

     // populate the fields
     student.firstName = firstName;
     student.lastName = lastName;
  
     // populate the function(s)
     student.getFullName = function()
     {
        return firstName + " " + lastName;
     }
     
     return student;
  }
现在我们可以调用这个函数来创建学生
  var student1 = createStudent("Joe", "Doe");

  var student1FullName = student1.getFullName(); // returns "Joe Doe"

  var student2 = createStudent("John", "Smith");

  var student2FullName = student2.getFullName(); // returns "John Smith"

对象 student1student2 将具有相同的结构但数据不同,就好像它们是通过在 C# 中使用相同的类 new 创建的一样。

有一种更好的方法来创建相同类型的对象,我们将在下面描述。每个函数都有一个“this”对象,它指定函数上下文。可以向其分配任何字段和任何值。当函数在运算符 new 前面被调用时:new <FunctionName>(...),在函数开始时会“new”出“this”对象,并由函数返回给调用者。这样的函数被称为 JavaScript 构造函数,类似于 C++、Java 和 C# 中的构造函数。因此,让我们使用“this”对象重写上面的函数(请注意,我将函数名称更改为 Student

  function Student(firstName, lastName, age) {
    // populate the fields
    this.firstName = firstName;
    this.lastName = lastName;

    // populate the function(s)
    this.getFullName = function () {
        return this.firstName + " " + this.lastName;
    }
  }
以下是我们如何使用操作符 new 调用这样的函数
  var student1 = new Student("Joe", "Doe"); // note that the syntax is the same as in OO languages
  
  var student1FullName = student1.getFullName(); // returns "Joe Doe"

请注意,现在我们可以使用 instanceof 操作符检查 student 对象是否由 Student 构造函数创建

  var student1 = new Student("Joe", "Doe"); 

  var isStudent = student1 instanceof Student; // isStudent is set to true

构造函数可以通过使用 <FunctionName>.call() 方法应用于已存在的对象,但此时 instanceof 运算符将不会显示该对象来自构造函数

  var student1 = new Object(); // create a student as an object, not by calling <code>Student</code> constructor

  Student.call(student1, "Joe", "Doe"); // "this" object of Student function 
                                        // is set by the first parameter to "call" function
                                        // (student1 in our case). Then Student function
                                        // populates the student1 object with the fields

  var student1FullName = student1.getFullName(); // student1FullName is set to "Joe Doe"

  var isStudent = student1 instanceof Student; // isStudent is set to false, since 
                                               // student was not created by using operator "new" 

关于 JavaScript 构造函数和“this”变量的更多信息

正如我们上面提到的,每个函数都包含 this 变量。它提供了调用函数的上下文。在对象外部调用的函数的 this 变量将设置为浏览器 window 对象
  AFunction(); // "this" variable within AFunction is set to global "window" object

如果函数在对象的上下文中调用,函数内的 this 变量将设置为调用该函数的对象

    var anObj = {
        aVar: 1,

        aFunction: function () {
            var canAccessObjectContext = this.aVar === 1; // checks if we can access aVar from anObj
        }
    };

    anObj.aFunction(); // function is accessed from within anObj context, "this" variable set to anObj 
                      //and canAccessObjectContext within the function is set to "true"

与 C#、Java 和 C++ 不同,JavaScript 函数中的 this 变量可以通过以下方式之一更改

  • 可以使用 call()apply 方法调用函数。在这种情况下,函数内的 this 变量被设置为传递的第一个参数
        var anObj = {
            aVar: 1,
    
            aFunction: function () {
                var canAccessObjectContext = this.aVar === 1; // checks if we can access aVar from anObj
            }
        };
      
        var anotherObj = {};
    
        anObj.aFunction.call(anotherObj, arg1, arg2); //"this" variable set to anotherObj
                                                      //and canAccessObjectContext within the function is set to "false"
    
  • 可以使用 bind() 方法克隆函数并为其提供不同的上下文
        var anObj = {
            aVar: 1,
    
            aFunction: function () {
                var canAccessObjectContext = this.aVar === 1; // checks if we can access aVar from anObj
            }
        };
      
        var anotherObj = {};
    
        var clonedFunction = anObj.aFunction.bind(anotherObj); //"this" variable set to anotherObj for the cloned function
    
        clonedFunction(); //canAccessObjectContext within the clonedFunction is set to "false"
    
    遗憾的是,IE8 不支持 bind()
  • 用于调用构造函数的运算符 new 将函数的上下文更改为一个新的空对象。

一个合理的问题是,如果有人忘记在构造函数前面加上 new 会发生什么。答案是,由于构造函数中的赋值,由 this 表示的某些上下文可能会损坏。

这种情况(开发人员忘记在构造函数前面加上 new)在JavaScript:不使用 new 调用构造函数中进行了详细考虑。事实证明,可以通过在构造函数开头添加以下检查来检测这种情况

  function Student(firstName, lastName, age) {
    if (!(this instanceof Student)) // check if the call to the 
                                    // constructor does not have "new" 
                                    // in front of it. 
    {
        alert("Programming Error: You forgot to put 'new' in front of the constructor");
        return; // if the call of the constructor does not have "new" 
                // in front of it, return undefined value
    }

    // populate the fields
    this.firstName = firstName;
    this.lastName = lastName;

    // populate the function(s)
    this.getFullName = function () {
        return this.firstName + " " + this.lastName;
    }
  }

我更喜欢在程序出错时提醒软件开发人员或 QA。然而,JavaScript:不使用 new 的构造函数中的解决方案展示了即使在没有 new 的情况下调用构造函数,如何创建一个正确的对象。

原型责任链和继承 

JavaScript 中的每个对象都有一个名为“prototype”的隐藏字段。当你尝试访问一个字段或函数,并且在主对象上找不到它时,它会检查其原型字段是否存在该字段或函数。如果在原型上找不到字段或方法,它会检查原型的原型,依此类推,直到遇到一个没有任何原型的对象。这种模式被称为“责任链”。

原型字段对开发人员是隐藏的,只能通过 JavaScript 构造函数设置。这是一个将 getFullName() 函数移动到原型并使其在 Student 构造函数创建的所有对象之间共享的示例

不是在原型上定义一些函数,而是可以将原型设置为一个通过 new 运算符获得的新对象。这将模仿继承

  function Student(firstName, lastName) {
    // populate the fields
    this.firstName = firstName;
    this.lastName = lastName;
  }

  // setting the prototype for the constructor 
  // will set the prototype to all the created object
  // to refer to function getFullName of the shared prototype
  Student.prototype.getFullName = function() {
        return this.firstName + " " + this.lastName;  
  }

  var student1 = new Student("Joe", "Doe"); // calling a constructor

  var student1FullName = student1.getFullName(); // student1FullName is set to "Joe Doe"

我们不是在原型上定义一些函数,而是可以将原型设置为一个通过 new 运算符获得的新对象。这将模仿继承

  // 'base class' is Student
  function Student(firstName, lastName) {
    // populate the fields
    this.firstName = firstName;
    this.lastName = lastName;

    // 'base class' defines a function accessible from
    // the 'derived class'
    this.getFullName = function () {
      return this.firstName + " " + this.lastName;
    }
  }

  // 'derived class' is HonorStudent
  function HonorStudent(firstName, lastName){ 
    // we have to redefine the fields
    // - since the prototype object is shared
    // it won't be able to have correct data for all
    // the "child" objects
    this.firstName = firstName;
    this.lastName = lastName;
  }

  // assign HonorStudent's prototype to be Student object
  HonorStudent.prototype = new Student();

  var student1 = new HonorStudent("Joe", "Doe"); // calling a constructor

  var student1FullName = student1.getFullName(); // student1FullName is set to "Joe Doe"

顺便说一句,学生的全名返回为“Joe Doe”这一事实也意味着 JavaScript 函数是“虚拟的”,因为它们使用“派生类”对象的字段而不是“基类”的字段。实际上,原型对象的 firstNamelastName 字段未定义(由于原型对象在不同对象之间共享,因此无法定义它们以满足所有可能的组合)。然而,“基类”中定义的函数 getFullName 返回正确的结果,适用于“派生类”的对象。

为了完全说服您这些函数在 C# 意义上是“虚拟的”,让我们在“基类”中引入另外两个函数,一个调用另一个,在“派生类”中覆盖被调用的函数,并确保在“派生类”上调用的调用者返回正确的值

  // 'base class' is Student
  function Student(firstName, lastName) {
    // populate the fields
    this.firstName = firstName;
    this.lastName = lastName;

    // 'base class' defines a function accessible from
    // the 'derived class'
    this.getFullName = function () {
      return this.firstName + " " + this.lastName;
    }

    // getLongDescription function calls getShortDescription function
    this.getLongDescription = function() {
       return "this is " + this.ShortDescription();
    }

    this.getShortDescription = function() {
       return "a Student";
    }
  }

  // 'derived class' is HonorStudent
  function HonorStudent(firstName, lastName){ 
    this.firstName = firstName;
    this.lastName = lastName;

    // redefine the function in the "derived class"
    this.ShortDescription = function() {
       return "an Honor Student";
    }
  }

  // assign HonorStudent's prototype to be Student
  HonorStudent.prototype = new Student();

  var student = new HonorStudent("Joe", "Doe"); // calling a constructor

  var studentLongDescription = student.getLongDescription(); // studentLongDescription is set to 
                                                             // "this is an Honor Student"

instanceof 运算符沿原型链向上传播,即如果当前对象不是由 instanceof 运算符右侧指定的构造函数创建的,它将检查其原型以及原型的原型,依此类推,直到找到由构造函数创建的原型,或者到达链的末端。因此,在上面的示例中,student instanceof HonorStudentstudent instanceof Student 都返回 true

在构造函数上调用 call() 函数不会设置原型,也不会使对象成为该构造函数的 instanceof,即如果我们在上面的代码中调用

  var student = new Object();
  HonorStudent.call(student, "Joe", "Doe");
学生将被设置为拥有名字和姓氏,但不会是 HonorStudentStudent 构造函数的 instanceof,也不会拥有来自原型的 getFullNamegetLongDescription 函数。

扩展数组以具有类似于 C# 的方法

原型也可以用于扩展对象而不修改它们的构造函数。您所需要做的就是将您想要的函数添加到对象构造函数的原型中,这些函数将出现在这些对象中。内置类型也可以通过这种方式扩展。例如,我缺少 C# 或 Java 集合中附带的与集合相关的方法,如 clear()remove() 等,所以我创建了一个 JavaScript 文件 ArrayExtensions.js 并将这些函数添加到 Array 功能中
// remove an element from an array
Array.prototype.remove = function (arrayElement) {
    var currentIndex = 0;
    do {
        if (this[currentIndex] === arrayElement) {
            this.splice(currentIndex, 1);
        }
        else {
            currentIndex++;
        }
    } while (currentIndex < this.length);
};

// insert an element at specified index
Array.prototype.insert = function (idxToInsertAfter, arrayElement) {
    this.splice(idxToInsertAfter, 0, arrayElement);
};

// return index of the first occurance of an element
Array.prototype.firstIndexOf = function (arrayElement) {
    var currentIndex = 0;
    do {
        if (this[currentIndex] === arrayElement) {
            return currentIndex;
        }

        currentIndex++;
    } while (currentIndex < this.length);
}

// return index of the last occurance of an element
Array.prototype.lastIndexOf = function (arrayElement) {
    var currentIndex = this.length - 1;
    do {
        if (this[currentIndex] === arrayElement) {
            return currentIndex;
        }

        currentIndex--;
    } while (currentIndex >= 0);
}

// clear all elements from an array
Array.prototype.clear = function () {
    this.length = 0;
}

// copies a subset of an array to a new array
Array.prototype.copy = function (beginIdx, numberElements) {
    if (!beginIdx) {
        beginIdx = 0;
    }
        
    var endIdx;

    if (!numberElements) {
        endIdx = this.length;
    }
    else
    {
        endIdx = beginIdx + numberElements;

        if (endIdx > this.length)
        {
            endIdx = this.length;
        }
    }

    var copiedArray = new Array();

    for(var i = beginIdx; i < endIdx; i++)
    {
        copiedArray.push(this[i]);
    }

    return copiedArray;
};

ArrayExtensions.js 文件位于 SimpleEventsJS 项目的 Scripts 文件夹下。

接口

JavaScript 中没有接口,也不需要它们。想一想,接口不提供新功能;它们通过强制开发人员对其进行编程并添加编译时兼容性检查来限制功能。在 JavaScript 中,可以将任何参数传递给任何函数——没有编译时兼容性测试,因此不需要接口。

将 C# LINQ 功能映射到 JavaScript

有一个名为 Underscore.js 的 JavaScript 库,它提供类似于 LINQ 的功能。您可以从 Underscore.js 下载该库,或者使用 NuGet 安装它。该网站还包含出色的文档。

Underscore 功能示例位于 UnderscoreTests 项目下。您必须在调试器中运行它以检查变量的状态,因为该项目不改变任何视觉信息。这是 JavaScript 代码

// student constructor sets firstName and lastName fields
// and getFullName function
function Student(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;

    this.getFullName = function () {
        return firstName + lastName;
    };
}

$(document).ready(function () {

    // we create a collection of student objects
    var collectionOfStudents =
    [
        new Student("Joe", "Doe"),
        new Student("Jim", "Smith"),
        new Student("Jane", "Smith"),
    ];

    // get all the students with last name "Smith"
    var studentsWithLastNameSmith =
        _(collectionOfStudents).filter(function (student) { return student.lastName === "Smith"; });

    // use chain() function to chain different operations (in our case it is filter() followed by map();
    // map will convert the Student objects returned by the filter() operation into strings containing
    // their last names
    // value() function will unwrap the object and return the array of last names of the 
    // selected students.
    var fullNamesOfStudentsWithLastNameSmith = _.chain(collectionOfStudents)
        .filter(function (student) { return student.lastName === "Smith"; })
        .map(function (student) { return student.getFullName(); })
        .value();
});

您可以看到 filter(...) 函数与 C# 中的 Where(...) 函数非常相似,而 map(...) 函数与 Select(...) 相同。

chain(...) 函数允许将 Underscore.js 运算符一个接一个地链接起来,就像在 LINQ 中通常所做的那样。

有关 Underscore.js 功能的更多详细信息,请查阅在线文档。

将 C# 和 WPF 事件功能映射到 JavaScript

WPF/Silverlight/C# 中有两种类型的事件 - 简单事件和路由事件。路由事件可以在可视化树中向上和向下传播,并且可以通过 JavaScript 中的文档事件进行模拟。JavaScript 中没有与简单 C# 事件对应的内置功能,但可以很容易地添加此功能。我们称此类事件为简单事件,以将其与 JavaScript 中内置的文档事件区分开来。

简单事件

SimpleEventsJS 项目包含一个简单事件示例。SimpleEvent.js 脚本为 JavaScript 添加了简单事件功能。它位于 SimpleEventsJS 项目的 Scripts 文件夹下。以下是 HelloWorld.html 如何使用该脚本中的功能

$(document).ready( // "ready" function ensures that 
                   // the functionality within is executed after the DOM is created
    function () {
        var mySimpleEvent = new SimpleEvent();

        // add an event handler to change the header text
        mySimpleEvent.addSimpleEventHandler(function () {
            $("#TheText").text("Yes, the first event handler is fired indeed!");
        });

        // add an event handler to add some text after the header
        mySimpleEvent.addSimpleEventHandler(function () {
            $("body").
               append("<div>Yes, the second event handler also fired!</div>");
        });

        $("#TheText"). // finds the HTML tag with "TheText" id
            text("Hello Wonderful World"); // changes the text within the tag

        // fire the simple event
        // (if you comment out this line the text within the browser stays
        //  "Hello Wonderful World", while if the event fires, the text will change).
        mySimpleEvent.fire();
    }
);

请注意,如果您注释掉 mySimpleEvent.fire() 行,您将看到与之前的 HelloWorldJS 示例相同的“Hello Wonderful World”消息。但是,如果您让 mySimpleEvent 触发,消息将被事件处理程序替换,您将看到以下内容

这是 SimpleEvent 功能的代码

function SimpleEvent() {
    this.eventHandlers = new Array();
};

SimpleEvent.prototype = {
    addSimpleEventHandler: function (eventHandler) {
        this.eventHandlers.push(eventHandler);
    },

    removeSimpleEventHandler: function (eventHandler) {
        this.eventhandlers.remove(eventHandler);
    },

    clearSimpleEventHandlers: function () {
        this.eventHandlers.clear();
    },

    setSimpleEventHandler: function (eventHandler) {
        this.clearEventHandlers();
        this.eventHandlers.addEventHandler(eventHandler);
    },

    fire: function (context, anyOtherArguments) {
        var result;

        var context;

        if (arguments.length > 0) {
            context = arguments[0];
        }

        var argsToPassToEvents = Array.prototype.copy.call(arguments, 1);

        for (var i = 0; i < this.eventHandlers.length; i++) {
            // apply function will execute the event handler, passing to 
            // it the context and an the arguments
            result = this.eventHandlers[i].apply(context, argsToPassToEvents);
        }

        return result;
    }
};

当然,与 C# 不同,无法确保某个开发人员不会访问 eventHandlers 数组并从中删除所有事件处理程序。

DOM 事件

DOM 事件就像 WPF 的路由事件一样,可以在 DOM 树中向上(冒泡)传播。JavaScript 中没有隧道事件的概念,但如果需要,可以添加类似隧道的功能。

内置事件示例

一个简单的 DOM 事件示例位于 BuiltInDomEvents 项目下。以下是代码中相关的 HTML 部分
<body>
    <h1 id="TheText">Hello World/<h1>
    
    <!-- we add a 'div' to demonstrate how the built-in click event works -->
    <div id="clickmeDiv" 
         style="width:200px; height:200px; background:red; float:left; font-size:60px">
        Click Me
    </div>
</body>

正如您所看到的,我们添加了一个名为“clickmeDiv”的 <div> 元素,大小为 200x200 像素,并将其着色为红色。

以下是使用 JQuery 的 bind() 函数将动作附加到 <div> 标签本身以及 <body> 标签更高级别的内置 JavaScript click 事件的 JavaScript 代码

$(document).ready(// "ready" function ensures that 
    // the function passed to it is executed after the DOM is created
    function () {
        $("body").bind("click", function () {
            alert("Clicked within body");
        });

        $("#clickmeDiv").bind("click", function (event) {
            alert("Clicked within 'clickmeDiv'");

            // to prevent the event from bubbling, 
            // uncomment "return false" line below
            // return false;
        });
    }
);

为什么使用 JQuery 的 bind() 函数为事件设置处理程序,而不是使用原生的浏览器 JavaScript 功能?因为原生浏览器 JavaScript 处理事件的功能在不同浏览器上略有不同,而 JQuery 方便地提供了一种通用的多平台实现方式。

现在,如果您运行示例,并点击红色方块,您将看到两个弹出的警报——一个在 <div> 级别,另一个在 <body> 级别,这意味着事件在 DOM 中冒泡。但是,如果 <div> 级别的事件返回 false,事件将阻止进一步冒泡,您将只看到一个警报。

JQuery 的 bind() 功能还可以在事件触发时禁用 DOM 元素的默认操作。默认操作是指当有人点击元素时执行的 HTML 操作。例如,对于超链接,这将是将 HTML 页面更改为链接的目标。事件处理程序可以通过返回 false 或调用 event.preventDefault() 函数来阻止它。

自定义 DOM 事件

较新的浏览器(但不幸的是不包括 IE 8)也支持使用 document.createEvent(...) 函数创建自定义 DOM 事件。WPF 和 Silverlight 软件开发人员可以将自定义 DOM 事件视为类似于自定义路由事件。

一个演示自定义 DOM 事件的示例位于 CustomDOMEvents 项目下。它的 HTML 部分与上一个示例几乎相同。以下是该示例的 JavaScript 代码

$(document).ready(// "ready" function ensures that 
// the function passed to it is executed after the DOM is created
    function () {

        if (!document.createEvent) {
            alert("this browser does not support custom event functionality. Please use a different browser");
            return;
        }

        // create a custom event
        var myCustomEvent = document.createEvent("Event");

        // initialize the custom event
        // 1st argument is the custom event name.
        // 2nd argument means that the event bubbles up the
        // DOM. 
        // 3rd argument means that the default action of the
        // event can be cancelled.
        myCustomEvent.initEvent("MyCustomEvent", true, true);

        // set the event handler for the custom event
        // at the body level
        $("body").bind("MyCustomEvent", function () {
            alert("Custom event is handled at the body level");
        });

        // set the event handler for the custom event
        // at the div level
        $("#TheDiv").bind("MyCustomEvent", function () {
            alert("Custom event is handled at the TheDiv level'");

            // to prevent the event from bubbling, 
            // uncomment "return false" line below
            // return false;
        });

        // Fire the custom event from the div element.
        // dispatchEvent should be called on
        // the DOM element itself, not on its JQuery wrapper.
        // We use $("#clickmeDiv")[0] to
        // extract a DOM element from the wrapper
        $("#TheDiv")[0].dispatchEvent(myCustomEvent);
    }
);

在支持自定义事件的浏览器中运行它,将弹出两个警报消息,表明事件在相应的 DOM 级别得到处理。

JQuery

我想以简要概述 JQuery 基础知识来结束本指南的这一部分。JQuery 是一个用于解析 DOM 和操作 DOM 元素的跨浏览器库。对于 WPF 和 Silverlight 开发人员来说,JQuery 和 Colin Eberhardt 的 LINQ to Visual Tree 库之间存在一些相似之处。

有关 JQuery 的详细文档,请参阅 JQuery.comapi.jquery.compluralsight.com 上也有许多 JQuery 课程,我强烈推荐。

JQuery 选择器和 DOM 操作 

JQuery 具有许多字符串模式,允许从 DOM 中选择一些元素。这些字符串模式称为选择器。

选择一堆 DOM 元素的函数看起来像这样:$(selector),其中“selector”是某个字符串。此函数总是返回一个 DOM 元素数组,即使您确定只返回一个元素,因此为了访问数组中的单个 DOM 元素,您必须使用索引器,例如 $("#theDiv")[0]

所有选择器都列在 JQuery Selectors 链接中。这里我们只考虑最重要的几个。

JQuery 示例位于 JQueryTests 项目下。以下是应用程序的 DOM 结构

<body>
    <h1 id="header"></h1>
    <div class="MyClass" style="width:700px; height:100px;font-size:40px;background-color:Yellow">
    </div>

    <div class="MyClass" style="width:700px; height:100px;font-size:40px;background-color:Aqua">
    </div>
    
    <div>
        <button style="width:250px;height:50px;"></button>
    </div>

    <div data-test="Test" style="width:800px; height:100px;font-size:30px;background-color:Gray">
        
    </div>

    <div id="ReplaceChildTest" style="width:200px; height:200px;font-size:30px;background-color:Green">
        <div id="ChildDiv" style="width:100px; height:100px;font-size:30px;background-color:Blue">
        </div>
    </div>
</body>
这是 JavaScript
$(document).ready(function () {
    // gets all the items with id="header" 
    // (since id is unique there can be only one item line that)
    // adds "Test ID Selector ..." text to the found dom element
    $("#header").text("Test ID Selector $(\"#header\")");

    // gets all elements of class .MyClass and sets
    // their text to "Test class selector ..." + index,
    // where index is the index of the element within the returned
    // array
    $(".MyClass").text(function (index, txt) {
        return "Test class selector $(\".MyClass\")   " + index;
    });

    // gets all "button" tags from the DOM 
    // and sets their text to "Test tag selector ..."
    $("button").text("Test tag selector $(\"button\")   ");

    // selects all the tags whose data-test attribute is set to 'Test'
    // and replaces text on them.
    $("div[data-test='Test']").text("Test attribute selector $(\"div[data-test='Test']\")");

    // remove the blue child div
    $("#ChildDiv").remove();

    // add red child div to ReplaceChildTest div.
    $("#ReplaceChildTest").append( "<div style='width:100px; height:100px;font-size:30px;background-color:Red'>");
});
正如您在 JavaScript 注释中看到的,我们给出了以下选择器的使用示例
  • $("#header") - 选择所有 id 等于“header”的 DOM 元素(只能有一个这样的元素)
  • $(".MyClass") - 选择所有 class 为“MyClass”的 DOM 元素
  • $("button") - 选择所有标签为“button”的 DOM 元素
  • $("div[data-test='Test'") - 选择所有“data-test”属性设置为“Test”的 DOM 元素

我们还看到,使用 text(...) 函数可以更改 JQuery 返回的每个元素下的文本。

此外,您可以使用 text(...) 函数的不同版本,利用 JQuery 传递给该函数的数组中 DOM 元素的索引

    $(".MyClass").text(function (index, txt) {
        return "Test class selector $(\".MyClass\")   " + index;
    });

利用这些索引,我们可以例如将文本更改为以索引结尾(如我们在示例中所做的那样),或者我们可以进行一些依赖于索引的处理,例如根据索引是奇数还是偶数来对条目进行不同的样式(尽管 JQuery 提供了更好的功能来实现这一点)。

最后,我们还展示了如何从 DOM 中删除或添加元素

    // remove the blue child div
    $("#ChildDiv").remove();

    // add red child div to ReplaceChildTest div.
    $("#ReplaceChildTest").append( "<div style='width:100px; height:100px;font-size:30px;background-color:Red'>");

摘要

在这篇文章中,我讨论了 JavaScript 和一些设计模式,这些模式对于具有 WPF/Silverlight/C# 背景的人来说会很熟悉。以下是一些最重要的要点
  • 原型与继承相关
  • 如上所示,简单的 C# 事件可以很容易地添加到 JavaScript 功能中
  • Underscore.js 库提供了 LINQ 功能。
  • DOM 映射到可视化树
  • JQuery 松散地映射到 LINQ to Visual Tree 功能
  • DOM 事件映射到路由事件
在这一部分,我主要关注 JavaScript 的非视觉方面。在后续部分,我计划进一步讨论视觉方面——SVG、JQueryui 库、使用带有 knockout.js 库的 MVVM、HTML5 Canvas、构建无外观自定义控件等。

历史

2012年9月11日 - 根据 Colin Eberhardt 的评论,添加了关于初始化对象和数组的首选方式的信息。还添加了更多关于 this 变量以及在构造函数调用前没有 new 操作符时会发生什么的信息。
© . All rights reserved.