Note for Professional JavaScript for Web Developers (1/4)

之前发现了一个前端技术栈的图,今天又听人介绍了这本JavaScript高级程序设计(第3版),之前大菜鸭也推荐了这本书的pdf,所以我决定再用这本书系统地过一遍JavaScript。从零开始学前端就得把前人啃过的书跟着啃一遍,特别是这样被反复推荐的经典书。开学再看看能不能跟着前端团队做些东西出来,实际操练一下,目前的简历一看就知道没写过多少前端代码,得让简历丰富起来才行。本篇对应1~5章。

JavaScript简介


  1. JavaScript的诞生
    起初JavaScript是Netscape公司为了改善浏览器处理表单验证的用户体验而设计的客户端语言。之后由于微软JScript的对峙,JavaScript语言的标准化语法和特性需要得到统一,欧洲计算机制造商协会ECMA完成了这个工作,所以日后人们通常把JavaScript和ECMAScript划了等号。但是JavaScript的含义其实比ECMA要多。
  2. JavaScript的三个组成部分;
    • ECMAScript: ECMAScript本身和Web浏览器没有依赖关系,浏览器、Node、Adobe Flash都可以作为宿主环境,宿主环境还能提供扩展以便语言和宿主之间对接交互。ECMAScript定义了语言的基础如语法、类型、语句、关键字、保留字、操作符、对象等,在此基础之上可以构建更完善的脚本语言。
    • DOM: Document Object Model文档对象模型是我看的前一本书的主题,它是针对XML但经过扩展用于HTML的应用程序编程接口,由DOM将文档映射成一个多层节点的抽象结构。利用DOM提供的API可以对页面中节点进行增删改查的各种操作,掌握了控制页面内容和结构的主动权。
    • BOM: Browser Object Model浏览器对象模型可以访问和操作浏览器窗口,控制显示的页面以外的部分,如新建、缩放、移动、关闭窗口、浏览器对象navigator、页面信息对象location、显示器对象screen、cookies支持、XMLHttpRequest等。BOM本身没有统一的标准,各个浏览器有不同的对象、属性、方法。而HTML5则将许多BOM写入正式规范。

在HTML中使用JavaScript


  1. <script>元素

    • 页面内嵌: 直接在标签之间插入JavaScript代码,会按照代码顺序执行。

      1
      2
      3
      4
      5
      <script type="text/javascript">
      function show() {
      alert("<\/script>");
      }
      </script>
    • 包含外部JavaScript文件: 利用src属性加载外部独立的.js文件,且按照顺序依次加载,只有前一个js代码解析完成后才会开始下一个。

      1
      2
      3
      4
      5
      <!--在XHTML和HTML中均可使用。此时前后标签之间的代码将被忽略-->
      <script type="text/javascript" src="js/xxx.js"></script>
      <!--也可以省略结束标签,但不符合HTML规范-->
      <script type="text/javascript" src="js/xxx.js" />
  2. <script>的位置
    由于浏览器在遇到<body>时才会开始呈现页面内容,如果把script引用的位置放到head中,只有当所有JavaScript代码都被下载、解析和执行完成后才会显示页面。所以<script>还是应当放到所有内容的后面、</body>之前。

  3. <script>的属性
    async, charset, defer, src, type(和已废弃的language)。
    • defer: 用于延迟脚本,只是用于外部脚本,下载后直到浏览器遇到</html>才会真正开始执行脚本。若多个脚本同时加入defer属性,HTML5规范中是要求脚本它们不一定会按照文档出现顺序执行。
    • async: 异步脚本也只用于外部脚本,控制页面不等待脚本的加载和执行,可以异步加载其他内容,这种情况下js代码中应尽量不涉及DOM操作。
  4. <noscript>元素
    当浏览器不支持JavaScript(现在不会了)或浏览器的JavaScript被停用时,需要让页面平稳退化,此时在<noscript>元素中的内容会显示出来。JavaScript正常的情况下,<noscript>元素中的内容不会显示。
  5. 文档模式
    文档模式从IE5.5开始引入,通过切换DOCTYPE实现。目前的文档模式有混杂模式、标准模式和准标准模式,区别在于CSS样式的呈现和部分JavaScript的解释执行。

基本概念


  1. 严格模式
    "use strict";: 使用严格模式可以为JavaScript定义一种不同的解析与执行模型,消除语法中本身的不合理、不严谨部分,防止不确定行为带来的潜在危害,还能一定程度提高执行效率。
  2. 分号与代码块
    • 虽然解析器可以帮助补充行末被省略的分号,但我们还是应当为每个语句加上分号,这样可以放心地删除多余的空白来压缩代码,同时也不用在解析时花时间推测在哪里插入分号、减轻了解析器的负担。
    • 虽然在多语句的情况下才必须使用花括号,但我们还是应当总是加上花括号形成代码块,这样便于清晰地展示编程意图,也方便修改代码块中的语句。
  3. 变量
    • ECMAScript中的变量是松散类型的,即不指定变量类型,变量可以保存任何类型的数据。可以使用typeof操作符来确定给定变量的类型,返回的是对应类型的字符串形式如”string”, “number”。
    • Undefined类型: 当变量没有声明或声明后没有被赋值时,使用typeof返回的类型都是undefined. Undefined类型的值只有undefined一个。
    • Null类型: Null类型的值也只有null一个,null值表示一个空对象指针,使用typeof(null)将返回object,因为null本身就指代一个对象指针。
    • Number类型: 整型默认是十进制,以0x开头为十六进制,以0开头为八进制,进行算术运算时都会转化为十进制。浮点数的小数点后必须为非零数,否则仍会解析为整型,同时浮点数的计算精确度不如整型,无法测试特定的浮点数值。使用e可以用科学计数法表示10次幂。当数值超过JavaScript定义的最大数值,会转化为Infinite值,该值将不会参与后续计算。而NaN值表示一个本来应该返回数值的操作未返回数值的情况,NaN参与的运算结果都是NaN,且NaN与任何值都不相等。
    • String类型: ECMAScript中的字符串值是不可变的,所谓的改变字符串值其实是将原字符串销毁后再创建一个新的字符串填充该变量。
    • Object类型: ECMAScript中Object是所有更具体对象的基础。每个Object实例都具有Constructor, hasOwnProperty(name), isPrototypeOf(obj), propertyIsEnumerable(name), toLocaleString(), toString(), valueOf()等属性和方法。
    • 将变量转为数字: 可用Number(s)(不支持八进制), parseInt(s)(支持十进制、八进制、十六进制,当然也可以提供第二个参数base来确定进制;不支持任何字符,从有效数字开始遇到非数字字符就停止parse返回整型), parseFloat(s)(只有十进制,忽略前导的所有0;从非零有效数字开始向后parse,只允许出现一个小数点;若小数点后全为0则返回整型)。
    • 将变量转为字符串: 可用x.toString()(除了null和undefined都可使用,对string使用则返回一个当前字符串的副本;可传入base参数指定进制,支持二进制、八进制、十进制、十六进制), String(x)(转型函数对所有变量都可使用,若传入变量支持toString()则直接调用,否则直接返回字面值如”null”, “undefined”)。
  4. 操作符
    • 一元加: 放在数值前面不会有什么变化,但放在非数值之前就会像Number()转型函数一样进行转换。
    • 位操作符: ~(NOT,按位非),&(AND,按位与),|(OR,按位或),^(XOR,按位异或),<<(左移,不影响符号位)、>>(有符号右移,不影响符号位)、>>>(无符号右移,影响符号位)。
  5. 语句

    • for语句: for(var i = 0; i < sum; i++)中声明的变量i也可以在循环外部访问到,这是因为ECMAScript没有块级作用域,所以循环内部的变量依然存在于外部。
    • for-in语句: 精准迭代语句,用于枚举:for (property in expression) ...
    • label语句: label: statement 为代码块添加标签,以便将来使用,通常会结合break或continue。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      outer:
      for (var i = 0; i < 10; i++) {
      for (var j = i; j < 10; j++) {
      if (xxx) {
      break outer;
      }
      count++;
      ...
      }
      }

    • with语句: with(expression) {statement}将代码的作用域设置到一个特定的对象中,可以简化多次编写同一个对象的工作。但是在strict模式下不允许使用

  6. 函数

    • 基本语法:

      1
      2
      3
      function foo(arg0, arg1, arg2, ...) {
      statements;
      }

    • 参数: ECMAScript的函数并不介意传入参数的个数、类型。在函数内部有一个arguments对象存储着传入的参数数组(只是类似于Array但不是Array的实例),若列表中的命名参数在调用时没有被赋值,则默认值是undefined。

    • 返回值: ECMAScript的函数在定义时并不需要指定是否有返回值,默认总是返回undefined。在实操中推荐维持一种编程习惯,要么所有函数都显示地return,要么全都不要返回值。否则会给调试带来很大不便。
    • 没有重载: 由于ECMAScript的函数的参数由零或多个值的数组来表示,所以函数签名(接受参数的类型和数量)不存在,就不可能为同名函数根据不同签名设置不同行为。不过在函数内部可以利用arguments来模仿重载的效果。

变量类型、作用域和内存


  1. 两种变量类型
    • 基本类型值: 简单的数据段,前面提到的Undefined, Null, Boolean, Number和String都是基本数据类型。可以直接操作存储在变量中的实际的值,所以是按值访问
    • 引用类型值: 保存在内存中的对象,JavaScript不允许直接访问内存中的位置,所以操作对象时实际是按引用访问
  2. 变量类型对比

    • 动态的属性: 对于基本类型,无法添加属性或方法(尽管不报错),访问属性、方法时也是undefined;对于引用类型则可以添加属性和方法。

      1
      2
      3
      var person = new Object();
      person.name = "Bobby";
      alert(person.name);
    • 复制变量值: 对于基本类型,将变量赋值给另一个变量会创建一个新值并把原值复制到新对象分配的位置上,两者互补干扰;对于引用对象,将变量赋值给另一个变量同样会创建一个新值并进行复制,但是由于这个副本其实是同一个对象的引用,所以两个变量其实指向的是同一个对象。

    • 参数传递: ECMAScript中所有函数参数都是按值传递,本质是在函数中创建局部变量,将传入的值按照复制变量值规则将传入的变量值复制给这些局部变量。因此完全可以将函数的参数看作函数体的局部变量。对于基本类型,按值传递较好理解;对于引用类型对象,确实是可以修改函数外传入的对象,这个现象很容易让人认为是按引用传递,但必须指出这依然是按值传递,可能要用下面的例子比较好理解:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function setName(obj) {
      obj.name = "Bobby";
      obj = new Object();
      obj.name = "liujb";
      }
      var person = new Object();
      setName(person);
      // 执行结果是Bobby,因为在函数内部修改引用值本身其实是对函数内部的局部引用类型变量修改
      // 而这个局部变量在退出函数时会销毁,当然不会让函数外的引用变量指向新的对象。
    • 类型检测: 使用操作符result = typeof x检测基本数据类型,使用result = x instanceof Constructor判断引用变量是否是某类对象的实例。

  3. 执行环境与作用域

    • 执行环境Execution context: 定义了变量或函数有权访问的其他数据,决定各自的行为。
    • 变量对象: 每个执行环境都有一个对应的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
    • 全局执行环境: 最外围的一个执行环境,不同的宿主环境对应的变量对象也不一样。对于web浏览器,全局执行环境是window对象,所有全局变量和函数都是window对象的属性和方法。当全局环境所处的应用程序终止后,全局执行环境才会销毁。
    • 函数执行环境: 每个函数都有自己的执行环境,当代码执行到一个函数时,函数的环境就会推入一个环境栈中,执行结束后会将函数环境弹出,将控制权换给之前的执行环境。
    • 作用域链scope chain: 当代码在一个环境中执行时便会创建变量对象的作用域链,保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的顶端是当前代码所在环境的变量对象,例如当这个环境是函数,则变量对象就是活动对象(activation object),一开始只有arguments变量。全局执行环境的变量对象是作用域链的最后一个对象。
    • 作用域链的延长: 在以下两种情况下会加长作用域链:
      -> try-catch语句的catch块会在作用域链顶端创建一个变量对象用于包含抛出的错误对象的声明;
      -> with语句将指定对象添加到作用域链顶端。
    • 没有块级作用域: JavaScript变量的创建和销毁是以执行环境为载体的,而不是由类C语言的花括号代码块。只要变量所依存的执行环境还存在,变量就能够正常被访问和修改。但需要注意的是,变量如果不用var声明而直接使用,这个变量会默认添加到全局环境中,形成全局变量(这在strict模式下是不允许的)。对于下面的例1,创建变量color的执行环境就是全局环境,所以在出了if语句后依然可以访问到。对于例2,变量sum是在函数中声明的,属于函数的局部环境,当函数执行完成后,这个局部环境会被销毁,就无法访问到sum了。对于例3,在非strict模式下,省去var声明直接使用变量sum会将其创建到全局环境中,所以离开函数后仍能访问到。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // 例1
      if (true) {
      var color = "green";
      }
      alert(color); // green
      // 例2
      function add(num1, num2) {
      var sum = num1 + num2;
      return sum;
      }
      var result = add(10, 20);
      alert(sum); // 报错,找不到变量
      // 例3
      function add(num1, num2) {
      sum = num1 + num2;
      return sum;
      }
      var result = add(10, 20);
      alert(sum); // 30
    • 查询标识符: 当某个环境遇到一个标识符(变量名)时,必须通过搜索确定它代表的是什么,搜索顺序从局部环境开始一直向外部环境延展,直到全局执行环境。一旦查找到对应变量值,就直接采用,这意味着局部环境中的同名标识符会暂时屏蔽掉父环境的标识符。

  4. 垃圾回收
    • 自动垃圾回收机制: JavaScript执行环境会负责管理代码执行过程中使用的内存,垃圾收集器会周期性地找出不再使用的变量并释放所占内存。
    • 两个GC策略:
      -> 标记清除mark-and-sweep: 当变量进入环境/离开环境时进行不同的标记。垃圾回收器在运行时会先将所有变量统一加上标记,然后抹去环境中的变量和被引用到的相关变量的标记,最后将仍有标记的值销毁,回收内存。
      -> 引用计数reference counting: 跟踪记录每个值被引用的次数,不太常用。当值被赋给一个变量时则+1,当这个变量取了另一个值的时候则-1。当引用次数降为0,则会在下一周期被回收。但是这个方法存在循环引用的问题,这回导致两个值的引用计数一直不是0,永不被回收导致内存泄漏。
    • 管理内存: 由于分配给web浏览器的内存通常比桌面程序少(防止运行JavaScript的网页耗尽资源导致系统崩溃),所以编程时应尽量优化内存占用,即只保留必要的变量和数据、在不用时设为null解除引用。这个做法是对全局变量和全局对象的属性而言,对于局部变量会在退出局部环境时自动销毁。

引用类型


  1. 引用类型值与类
    • 引用类型和类常被混为一谈,但必须指出虽然ECMAScript是面向对象的语言,但不具备传统面向对象语言所支持的类和接口等结构,因此不能把二者划上等号。
    • 引用类型的实例就是对象(或引用类型的值)。
    • 引用类型是一种数据结构,用于将数据和功能组合在一起。
  2. Object类型

    • Object: 使用最多的一个类型,大部分引用类型值都是Object类型的实例。
    • 创建Object实例的方式: new操作符 和 对象字面量。其中用对象字面量定义对象时,不会调用Object构造函数。更常用的是对象字面量法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      // new操作符
      var person = new Object();
      person.name = "Bobby";
      person.state = "single";
      // 对象字面量: var person = {
      name: "Bobby",
      state: "single"
      }
      ```
      * 访问属性:
      点表示法 和 方括号表示法。其中方括号表示法允许通过字符串变量来访问属性,字符串中还可以包含空格等特殊字符,这在点表示法中是办不到的。不过除非特殊字符,通常
      还是用点表示法更直观。
      ```JavaScript
      alert(person.name);
      var propertyName = "first name";
      alert(person[propertyName]);
      ```
      3. Array类型
      * Array:
      使用频率仅次于Object,与其他语言的数组的重大区别是,ECMAScript的数组中的每一项可以是不同类型的。同时数组的大小是可以自动增长来容纳新增数据。
      * 创建数组的方式: Array构造函数 和 数组字面量表示法。使用数组字面量表示法也不会调用到数组的构造函数。
      ```JavaScript
      // 构造函数。其中的new可以省略不写。
      var colors = new Array(20);
      var colors = new Array("red", "blue", "green");
      // 字面量
      var colors = ["red", "blue", "green"];
    • length属性: 数组的length属性不是只读的,可以通过修改length值删除末尾元素或手动增加数组长度(新增位的值为undefined)。通过给arr.length位置赋值也可以方便地向数组末尾插入一个新项。

    • 数组对象的判别: instanceof操作符 和 Array.isArray()方法。

      1
      2
      3
      4
      5
      // ECMAScript 3+
      if (value instanceof Array)
      // ECMAScript 5+
      if (Array.isArray(value))
    • toString, join, toLocaleString和valueOf方法: 所有对象都具有这三个方法。对数组使用valueOf()返回的仍是数组;对数组使用toString()相当于对每一项分别使用toString()后再用逗号拼接起来;使用join()可自定义连接符返回字符串,如arr.join("~");,若不传入任何字符则默认是逗号。

    • 栈方法:
      -> push(): 可接受任意个参数并逐个添加到数组末尾,返回修改后的数组长度;
      -> pop(): 从数组末尾移除一项,返回被移除的值。
      这些操作都发生在末尾(栈顶),使得数组的行为与栈LIFO非常类似。
    • 队列方法:
      -> push(): 同栈方法push。
      -> shift(): 从数组的最前端取出项,并将该值返回,数组长度减1.
      结合使用上述两种方法可以正向模拟队列。
      -> unshift: 向数组的最前端插入项,返回修改后的数组长度;
      -> pop(): 同栈方法pop。
      结合这两种方法可以从相反的方向模拟队列。
    • 逆序: 直接使用arr.reverse();将数组倒转。
    • 排序: 使用arr.sort();会将原数组的每一项都使用toString()后再进行升序排序,即使每一项都是数值。我们可以定义一个比较函数作为参数传入sort函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function cmp(value1, value2) {
      if (value1 < value2) {
      return -1;
      } else if (value1 > value2) {
      return 1;
      } else {
      return 0;
      }
      }
    • 拼接: 使用arr.concat(arg1, ...);将给定内容拼接到当前数组的副本后面,若所给是数组,则会取出数组中的每一项插到最后。concat执行后不会影响原数组,返回的是新创建的数组。

    • 切片: 使用arr.splice(start, end);将给定范围(不包括结束尾;或者可以不传入结束位)的子数组创建出来,不影响原数组。
    • 全能操作方法splice:
      -> 删除: splice(start, length)指定第一项开始删除的索引和总共删除的项数。
      -> 插入: splice(pos, 0, insertItems...)指定要插入的位置、要删除的项数和对应要插入的项。
      -> 替换: splice(pos, deleteNum, insertItems...)指定起始位置、要删除的项数和要插入的项。
      splice执行后会影响原数组,并始终都会返回一个包含了所删除项的数组。
    • 查找元素位置:
      -> arr.indexOf(item, start): 指定要查找的项和查找的起始索引。
      -> arr.lastIndexOf(item, start): 与上面相似,只是会从后往前进行查找。
    • 迭代方法:
      -> every(func): 对数组的每一项执行给定函数,结果全为真才返回true.
      -> some(func): 对数组的每一项执行给定函数,结果有一项为真就返回true.
      -> forEach(func): 对数组的每一项执行给定函数,无返回值。
      -> map(func): 对数组的每一项执行给定函数,返回每一项的运行结果组成新的数组。
      -> filter(func): 对数组的每一项执行给定函数,将结果为false的元素过滤掉后将剩余元素组成新数组返回。
    • 缩小方法: reduce()和reduceRight()都用于缩小,区别只是方向是正向还是逆向。接受1~2个参数,分别是要调用的函数和作为缩小基础的初始值。所传入的函数接受4个参数,分别是前一项值、当前值、项的索引和数组对象。该函数的返回值会作为第一个参数传入下一次执行。
      1
      2
      3
      4
      // 对数组values进行求和
      var sum = values.reduce(function(prev, cur, index, array) {
      return prev + cur;
      });
  3. Date类型

    • Date: 在早期Java中的java.util.Date基础上构建。以毫秒为单位保存从1970.1.1的前后285616年的日期。
    • 创建Date实例:
      -> 不传参时会自动获取当前日期和时间: var now = new Date();
      -> 指定日期可传入Date.parse()或Date.UTC()解析出的毫秒数。

      1
      2
      3
      4
      5
      // 2017.5.20 0:00:00
      var hopefully = new Date(Date.parse("Tue May 20 2017 00:00:00 GMT-0700"));
      // 2015.5.5 17:55:55 注意月份从0开始计(这是军用方式...反人类啊)
      var another = new Date(Date.UTC(2015, 4, 5, 17, 55, 55));
    • Date实现计时器: 在ECMAScript 5中添加了Date.now()方法获取当前毫秒数;而在不支持的浏览器中,简单地使用一个+操作符即可将Date对象转换成数值,也能达到计时效果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 方法1
      var start = Date.now();
      ...
      var stop = Date.now();
      var time = stop - start;
      // 方法2
      var start = +new Date();
      ...
      var stop = +new Date();
      var time = stop - start;
    • Date的格式化输出: 除了继承的toString, toLocaleString, valueOf, 还有toDateString, toUTCStirng, toTimeString等。

  4. RegExp类型

    • RegExp: 创建正则表达式,为var exp = /pattern/ flags; 其中flags有三种:
      -> g: 全局模式,发现第一个匹配项后不停止,继续应用于后续字符串。
      -> i: 匹配时忽略大小写。
      -> m: 遇到换行时不停止,继续向后查找是否有匹配项。
    • 创建RegExp类型实例: 字面量定义 和 RegExp构造函数。构造时接受两个参数,即需要匹配的字符串模式和标志位。不过需要注意的是由于字符串中可能需要很多转义,所以用起来不如字面量方便。

      1
      2
      3
      4
      5
      var exp1 = /[bc]at/i;
      var exp2 = new RegExp("[bc]at", "i");
      var exp3 = /\d.\d{1, 2}/;
      var exp4 = new RegExp("\\d.\\d{1, 2}");
    • 常用的正则表达方法:
      -> 若直接给出字符,则是精确匹配。对于特殊符号如-_等,需要加上转义符\
      -> \d匹配一个数字,\w匹配一个字母或者数字,\s匹配一个空格或其他空白符,.匹配任意一个字符。
      -> 上面只能针对一个字符进行匹配,若要指定数量,*表示任意个字符(包括0),+表示至少一个字符,?表示0或1个字符,{n}表示n个字符,{n, m}表示n~m个字符。
      -> []可以给定范围,例如[0-9a-zA-Z\_]可以匹配一个数字、字母或下划线。可以利用上一点中的符号来指定匹配数量,如[0-5a-eA-E]+
      -> ^可以指定开头符号,如^\d可指定以数字开头。
      -> $可以指定结尾符号,如\d$可指定以数字结尾。

    • RegExp实例的方法:
      -> exec(): 传入需要核对样式的字符串,返回包含匹配项信息的数组。这个数组除了Array实例都有的属性,还有index属性(表示匹配项在字符串中的位置)和input属性(应用正则表达式的字符串,即传入exec的那个字符串)。若没有匹配则返回null;若正则表达式中含有小括号括起来的捕获组,则会依次放入数组的后续项中;若无捕获组,则数组只有第一项完全匹配的。
      -> test(): 传入需要核对样式的字符串,根据是否匹配返回布尔值。在只需要知道字符串是否符合某种规则而不需要提取内容时,使用test很方便。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // exec
      var text = "mom and dad and baby";
      var pattern = /mom( and dad( and baby)?)?/gi;
      var matches = pattern.exec(text);
      // 由于存在捕获组,故返回的数组matches含有三项
      // [0]="mom and dad and baby"
      // [1]=" and dad and baby"
      // [2]=" and baby"
      // 若exec的正则表达式不是全局模式,它始终只能找到第一个匹配项
      var text = "cat, hat, bat, fat";
      var pattern1 = /.at/;
      var pattern2 = /.at/g;
      var matches1 = pattern1.exec(text); // 执行多少次都只有cat
      var matches2 = pattern2.exec(text); // 执行一次得到cat,再执行依次得到hat bat fat
      // test
      var text = "123-45-6789";
      var pattern = /\d{3}-\d{2}-\d{4}/;
      if (pattern.test(text)) {
      ...
      }
  5. Function类型

    • Function: 在ECMAScript中,函数也是对象,每个函数都是Function类型的实例。
    • 创建Function类型实例: 使用 函数声明语法 、函数表达式 和 构造函数来定义。由于使用构造函数来定义会导致多一次解析(除了JS代码解析,还需要解析字符串取出参数等),影响性能,而且不直观,所以不推荐第三种方式。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function sum( num1, num2 ) {
      return num1 + num2;
      }
      var sum = function( num1, num2 ) {
      return num1 + num2;
      }
      var sum = new Function("num1", "num2", "return num1 + num2");
    • 没有重载: 函数是对象,函数名是指针。因此对相同的函数名进行新一轮的赋值,赋予了另一个函数的地址,那么原来的那个函数就无法通过这个函数名(变量)访问了,因此JavaScript没有重载。

    • 函数声明提升function declaration hoisting:
      解析器会率先读取函数声明,保证函数在执行任何代码之前能用(accessible);而函数表达式的方式,即将函数赋值给变量的方式必须等到解析器执行到所在的代码行才会真正解析执行。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 函数声明方式,可以正常运行
      alert(sum(10, 10));
      function sum(num1, num2) {
      return num1 + num2;
      }
      // 函数表达式方式,报错,未声明的函数
      alert(sum(10, 10));
      var sum = function(num1, num2) {
      return num1 + num2;
      }
  6. 作为值的函数 function as value

    • 既然函数名可以作为变量,那么也就可以作为参数传入另一个函数,构建一个通用型的函数(不过暂时还没有体会到这个技术有什么厉害的…)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function callSomeFunction(someFunction, someArgument) {
      return someFunction(someArgument);
      }
      function add10(num) {
      return num + 10;
      }
      var result = callSomeFunction(add10, 50);
    • 也可以将一个函数作为另一个函数的返回值。例如我们想为sort构造一个比较函数,我们需要一个函数来指明根据哪个属性来比较排序,这样就可以通过传入不同的属性名获得不同的比较函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      function createCmp( propertyName ) {
      return function( object1, object2 ) {
      var value1 = object1.propertyName;
      var value2 = object2.propertyName;
      if (value1 < value2) {
      return -1;
      } else if (value1 > value2) {
      return 1;
      } else {
      return 0;
      }
      };
      }
      // 在排序时cmp就可以现做现用
      var data = [{name: "a", age: 8}, {name: "b", age: 18}];
      data.sort(createCmp("name"));
      data.sort(createCmp("age"));
  7. 函数内部属性(or内部对象)

    • arguments: 前面有提到,这是一个类数组对象,保存着函数执行时传入的所有参数。它还有一个callee属性,callee指针指向拥有这个arguments对象的函数。但在strict模式下访问arguments.callee会报错!

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // 经典的阶乘函数,与函数名耦合性很强
      function factorial(n) {
      if (n <= 1) {
      return 1;
      } else {
      return factorial(n - 1) * n;
      }
      }
      // 如果将函数起个新名字(赋值给另一个变量)然后销毁,这个函数就不能正常工作了
      var newFactorial = factorial;
      factorial = function {
      return 0;
      }
      // 解除这种耦合就可以使用arguments.callee
      function factorial(n) {
      if (n <= 1) {
      return 1;
      } else {
      return arguments.callee(n - 1) * n;
      }
      }
    • this: 引用的是函数据以执行的环境对象,是什么环境调用函数,该函数就可以通过this访问存在于该环境对象的属性、方法等。需要指出的是下面代码中sayColor只有一个,函数名只是函数的指针,即使由不同的环境对象执行,也调用的是同一个函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      window.color = "red";
      var o = { color: "blue"};
      function sayColor() {
      alert(this.color);
      }
      o.sayColor = sayColor;
      sayColor(); // 访问的是window对象的color
      o.sayColor(); // 访问的是o对象的color
    • caller: 在ECMAScript 5中加入,保存调用当前函数的函数引用;若是在全局作用域中调用当前函数,则值为null。但在strict模式下,caller同样不可用,以防止第三方脚本窥探环境中的其他代码。

      1
      2
      3
      4
      5
      6
      7
      8
      function outer() {
      inner();
      }
      function inner() {
      alert(arguments.callee.caller);
      }
      outer(); // 弹窗中会显示outer函数的源码
  8. 函数的属性和方法

    • length属性: 函数定义中接收的命名参数的个数。
    • prototype属性: 对于ECMAScript的引用类型而言,prototype是保存他们所有实例方法的真正所在,只不过是通过各自对象实例去访问而已。在创建自定义引用类型以及实现继承时,prototype很有用。
    • apply()方法: 在特定的作用域中调用函数,等于设置函数体内this对象的值,这样一来对象就不需要与方法有耦合关系。
      接受两个参数,一是其中运行函数的作用域,然后是参数数组(arguments对象或Array实例都可以)。注意在strict模式下,若没有指定环境对象直接调用函数,则this值不会转型为window,除非明确把函数添加到某个对象或调用apply或call。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function callSum1(num1, num2) {
      return sum.apply(this, arguments);
      }
      function callSum2(num1, num2) {
      return sum.apply(this, [num1, num2]);
      }
      alert(callSum1(10, 10));
      alert(callSum1(20, 20));
    • call()方法: 作用与apply相同,都是为了在特定作用域中调用函数、扩充函数赖以运行的作用域。与apply的区别只是传入第二个参数时,不能使用数组,必须将所有参数拆开来依次传入。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      window.color = "red";
      var o = { color: "blue"};
      function sayColor() {
      alert(this.color);
      }
      sayColor.call(this); // 指向window对象的color
      sayColor.call(window); // 显式指向window对象的color
      sayColor.call(o); // 显式指向o对象的color
    • bind()方法: 定义于ECMAScript 5,会创建一个函数的实例,其this值会被绑定到传入bind函数的值上,最后将这个绑定完成的函数返回。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      window.color = "red";
      var o = { color: "blue" };
      function sayColor() {
      alert(this.color);
      }
      var objectSayColor = sayColor.bind(o);
      objectSayColor(); // 已绑定到o,故显示blue.
  9. 基本包装类型

    • 特殊的引用类型: 为了便于操作基本类型值,ECMAScript还提供了Boolean, Number和String。每当读取一个基本类型值时,后台都会创建一个对应的基本包装类型对象。讲道理,基本类型值不是对象,不应该有方法,所以为了能方便我们调用方法,后台实际上为我们创建了对应的基本包装类型对象。
    • 引用类型与基本包装类型的区别: 对象的生存期不同。
      -> 使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都会一直存在,我们可以在作用域内放心地添加、访问属性和方法;
      -> 自动创建的基本包装类型对象只存在于代码执行的那一瞬间,执行后立即销毁,所以不能在运行时添加属性和方法。
    • Boolean类型: 与布尔值对应的引用类型。永远不要用Boolean对象!因为布尔表达式中的所有对象都会被转化为true,这意味着即便声明时用var falseObject = new Boolean(false);,在判断时都会被转化成true,因此falseObject && true的结果是true。
    • Number类型: 与数字值对应的引用类型。提供了若干将数字格式化地转化为字符串的方法。
      -> toString(base): 可根据基数转为特定进制的数字字符串。
      -> toFixed(bit): 按照指定的小数位返回数字字符串,具有四舍五入的特性。
      -> toExponential(bit): 按照指定的小数位返回科学计数法的数字字符串。
      -> toPrecision(bit): 可能返回fixed也可能返回exponential格式,传入的bit包括所有数字的位数而不只是小数点之后的位数。
    • String类型: 与字符串对应的引用类型。提供了取字符、比较、拼接、切割、取子串、位置方法、大小写转换、正则匹配、查找等方法。
      -> charAt(pos), charCodeAt(pos): 接受字符位置index,返回单个字符的字符串/字符码(ECMAScript没有字符类型)。在ECMAScript 5中还可以用方括号来访问。
      -> String.fromCharCode(): 静态方法,直接通过类名调用(虽然ECMAScript中没有类,不过就先这么理解吧)。传入一系列字符编码,返回由这些编码对应的字符组成的字符串。
      -> localeCompare(): 传入一个字符串,根据字母表的顺序,若应排在字符串参数之后为正数、之前为负数、相等为0.
      -> concat(): 传入若干字符串,将一个或多个字符串拼接到原字符串后面,返回新字符串,不会影响原字符串。在实践中直接使用+号来拼接字符串更方便直观。
      -> slice(), substring(): 传入起始index和结束index(可选,不包含),返回子字符串,不会影响原字符串。
      -> substr(): 传入起始index和截取长度(可选),返回子字符串,不会影响原字符串。
      -> split(): 基于指定的分隔符或正则表达式将一个字符串分割成子字符串并存入数组中返回。
      -> indexOf(), lastIndexOf(): 从字符串中搜索给定的子字符串,返回第一次出现位置,若无则为-1。二者区别只在于是从前往后搜索还是从后往前搜索。可选择传入第二个参数指定起始位置。
      -> trim(): 将原字符串开头和结尾的空格全部删除,返回新建的字符串,不会影响原字符串。
      -> toUpperCase(), toLowerCase()及二者Locale版: 返回大小写转换后的字符串,不会影响原字符串。
      -> match(): 传入正则表达式或RegExp对象,返回匹配数组。本质上与调用RegExp对象的exec()方法相同,只是这个的执行者变成了字符串。
      -> search(): 传入正则表达式或RegExp对象,返回字符串中第一个匹配项的索引。
      -> replace(): 传入RegExp对象或一个字符串和一个字符串或函数这两个参数。其中当第二个参数为函数时,对于只有一个匹配项,这个函数会接受到三个参数——模式的匹配项、模式匹配项在字符串中的位置和原字符串;对于正则表达式中含有多个捕获组的情况,这个函数会接收到模式的匹配项、第一个捕获组的匹配项、第二个…和模式匹配项在字符串中的位置和原字符串。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      var text = "cat, hat, bat, fat";
      var result = text.replace("at", "ond");
      // 得到"cond, hat, bat, fat"
      var result = text.replace(/at/g, "ond");
      // 得到"cond, hond, bond, fond"
      function htmlEscape(text) {
      return text.replace(/[<>"&"]/g, function(match, pos, originalText)) {
      switch(match) {
      case: "<"
      return "&lt;";
      case: ">"
      return "&gt;";
      case: "&"
      return "&amp;";
      case: "\""
      return "&quot;";
      }
      });
      }

    此外字符串类型还有一些HTML方法,便于使用JavaScript动态格式化HTML。这里就不写了。其实这个方法感觉并不使用,设置粗体、斜体等不应该通过HTML来做,应该交给CSS。

  10. 单体内置对象
    • 内置对象的定义: 由ECMAScript实现提供的、不依赖于宿主环境的对象。不需要显示地实例化内置对象,它们在程序执行之前就已经实例化了。前面的Object, Array, String都是内置对象。此外还有两个单体内置对象: Global和Math。
    • Global: 最保底的对象,所有不属于任何其他对象的属性和方法,最终都是Global对象的例如isNaN(), isFinite(), parseInt(), parseFloat(),此外还有一些方法也是Global的。
      -> URI编码方法: 对uniform resource identifiers通用资源标识符进行编码,可使用encodeURI()和encodeURIComponent(),防止无效字符在浏览器中无法识别。encodeURI()主要应用于整一个URI,除了空格会处理其余字符都不会改变;encodeURIComponent()主要用于其中某一段,会对任何非标准字符进行转换,在实践中我们更常用到它来对查询字符串参数进行编码。
      -> URI解码方法: decodeURI()和decodeURIComponent()。
      -> eval()方法: 在strict模式下eval会受很大限制。正常模式下,eval接收一个完整的JavaScript语句,被eval包围的语句会被当作真实的JavaScript语句来执行,在eval中可以定义变量和函数并在外部访问到,而strict模式下不允许,这就防止了代码注入。
      -> window对象: 在浏览器中,window对象可视为Global对象。
    • Math:
      保存了一些常用的数学特殊值和数学方法。
      -> 属性: Math.PI, Math.E(自然对数), Math.LN10(ln10), Math.LN2(ln2), Math.SQRT2(2的平方根等)。
      -> 数学方法: Math.abs(n), exp(n), log(n), pow, sqrt, acos等。
      -> Math.min()/max(): 传入任意个参数,取最小/最大值。而对于数组,可以利用apply来设置this值:var max = Math.max.apply(Math, arr);
      -> Math.ceil()/floor()/round(): 向上、向下和四舍五入方法。
      -> Math.random(): 生成一个0-1之间的随机数(不含0和1)。例如生成某个范围[a, b]的数,可以写成一个固定的函数:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function generateRandomNum(a, b) {
      var lower = a, upper = b;
      if (lower > upper) {
      var temp = lower;
      lower = upper;
      upper = temp;
      }
      var choices = upper - lower + 1;
      return Math.floor(Math.random() * choices + lower);
      }

To be continued…