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

JavaScript高级程序设计(第3版)这本书的内容真的有点多,笔记得分成几个部分,不然挤在一篇里面就太长了不方便看。这是第二篇笔记,得加快点进度了。本篇对应6~8章。

面向对象的程序设计


  1. JavaScript中的对象

    • ECMA定义: 对象是无序属性的集合,其中属性可以包含基本值、对象或者函数。每个对象都基于一个引用类型值创建。由于ECMAScript中没有类的概念,这里说的对象也就和那些基于类的语言不太一样了。
    • 创建对象: 可以采用new Object()的形式;但更常用的是对象字面量的形式。
    • 数据属性: 每个property都有各自的attribute,描述了property的配置信息。
      -> configurable: 能否通过delete来重新定义属性、修改属性的特性等。默认为true.
      -> enumerable: 能否通过for-in循环返回属性。默认为true.
      -> writable: 能否修改属性的值。默认为true.
      -> value: 存储这个属性的数据值。默认为undefined.
      要想修改property的attribute,必须使用Object.defineProperty()静态方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      var person = {};
      Object.defineProperty(person, "name", {
      writable: false,
      value: "Bobby"
      });
      // 此时name属性是只读的。
      person.name = "JB"; // 在strict模式下修改只读属性会报错,正常模式则直接忽略此句
      Object.defineProperty(person, "name", {
      configurable: false,
      });
      // 此时name不可配置。无法修改除了writable的其他attribute。所以无法再把config改回true了。
      delete person.name; // 无法删除name属性
    • 访问器属性: 不包含数据值属性。含有一对getter和setter函数,不过倒也不是必须含有的。除了configurable和enumerable,访问器属性还有get和set两个属性。只能使用Object.defineProperty()静态方法来定义和修改访问器属性。
      -> get: 读取属性时调用的函数。默认为undefined.
      -> set: 写入属性时调用的函数。默认为undefined.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      var book = {
      _year: 2014, // 以下划线开头就表示这个变量必须通过对象方法来访问和修改
      edition: 1
      }
      Object.defineProperty(book, "year", {
      get: function() {
      return this._year;
      },
      set: function(newYear) {
      if (newYear >= 2014) {
      this._year = newYear;
      this.edition = (newYear - 2014); // 这不是书上的源码,我感觉中文版有错,不知原书如何。
      }
      }
      });
    • 修改多个属性的特性: 为了方便同时修改多个properties的attributes,ECMAScript 5引入了Object.defineProperties()方法,用法与Object.defineProperty类似。

    • 读取属性的特性: ECMAScript 5提供了Object.getOwnPropertyDescriptor()来取得给定属性的描述符,接受两个参数:对象名+属性名。
  2. 创建对象

    • 前面提到了Object构造函数和对象字面量两种方式来创建单个对象,但是使用同一个接口会创建出很多对象,产生大量重复代码。
    • 工厂模式: 抽象出了创建具体对象的过程,用函数封装以特定接口创建对象的细节。这个方法解决了多个相似对象代码重复的问题,但是无法解决对象类型识别问题。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function createPerson(name, age, job) {
      var o = new Object();
      o.name = name;
      o.age = age;
      o.job = job;
      o.sayName = function() {
      alert(this.name);
      };
      return o;
      }
      var person1 = createPerson("A", 18, "driver");
    • 构造函数模式: 利用ECMAScript的函数声明提升特性,创建自定义的构造函数,在代码运行之前就存在于环境中。注意在构造函数中没有显示地创建对象,自然也就不需要return具体对象了。创建对象会经历一下四个阶段:(1)new出一个新对象;(2)将构造函数的作用域赋给新对象(this也会指向它);(3)执行构造函数代码;(4)返回新对象。事实上任何函数在前面加个new操作符,都可看作构造函数。构造函数的不足就在于每个方法都会在每个对象实例中重新创建一遍,每个对象的函数都是不同的函数对象(即便代码一模一样)。解决的办法是把函数代码放到构造函数外,相当于全局函数,而在对象内部只是赋予一个引用。这样暂时解决了重复函数对象的问题,但又导致封装型被破坏。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
      o.sayName = function() {
      alert(this.name);
      };
      }
      // 当构造函数使用
      var person1 = new Person("A", 18, "driver");
      // 当普通函数使用,此时只是将其中的属性和方法添加到全局环境,即window中
      Person("A", 18, "driver");
      // 在另一个对象的作用域中调用
      var o = new Object(); Person.call(o, "A", 18, "driver");
    • 原型模式: 每个函数都有prototype属性,指向一个原型对象。使用原型对象让所有对象实例共享它包含的属性和方法。原型对象会自动获得一个constructor属性,指向的就是”指向这个原型对象”的函数。可使用obj.isPrototypeOf(obj)来判断前者是否是后者的原型对象。
      对于每一个对象实例来说,都是可以访问到原型对象的属性的,但是不能通过实例去修改对应的原型对象中的属性。对实例添加同名的属性和方法时,对于这个实例会暂时将原型对象的同名属性和方法屏蔽掉,直到使用delete。可使用obj.hasOwnProperty()来判断属性来自实例本身还是它的原型。in操作符则是只要属性能访问到,无论是本身还是原型,都返回true。
      原型对象有动态性,我们对原型对象做的任何修改都会立刻从各个实例上体现出来。但是需要指出,如果是直接对prototype整个进行重新赋值,那就相当于切断了构造函数与最初原型之间的联系。在这种情况下,更新的原型就不会体现在原有的实例上了,它们指向的还是原来的原型对象。实例中的指针仅指向原型,而不指向构造函数
      原型模式存在的问题也就在于它共享的本性,尤其是对于引用类型的属性,例如Array数组在某一实例中的修改会体现在另外的实例中,这显然是不可接受的。

      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
      function Person() {} // 即便构造函数是空的也能正常工作,因为实例访问属性和方法时会不断向上搜索直到原型对象。
      Person.prototype.name = "Bobby";
      Person.prototype.age = 21;
      Person.prototype.job = "???"; // 只是为了演示才把属性也放到原型中。讲道理,这么写不合常理。
      Person.prototype.sayName = function() {
      alert(this.name);
      };
      // 更简洁的写法看这里,但是这相当于重建了Person的原型,这会导致constructor不再指向Person。
      // 不过你也可以手动添加constructor: Person, 只不过会把enumerable改成true。而默认是不可遍历的。
      /*
      Person.prototype = {
      name: "Bobby";
      age: 18;
      job: "???";
      sayName = function() {
      alert(this.name);
      }
      };*/
      var person1 = new Person();
      var person2 = new Person();
      // 可结合hasOwnProperty和in操作符判断属性是否在原型对象中
      function hasPrototypeProperty(object, propertyName) {
      return !object.hasOwnProperty(propertyName) && (propertyName in object);
      }
    • 组合使用构造函数模式和原型模式: 这是目前使用最广泛、认同度最高的用法。构造函数模式用于定义实例属性,原型模式用于定义方法和少量共享的属性。这样可以保证每个实例都拥有自己独立的实例属性(包括引用类型),同时又共享了对方法的引用,节省了内存。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
      this.friends = ["A", "B"];
      }
      Person.prototype = {
      constructor: Person,
      sayName: function() {
      alert(this.name);
      }
      }
    • 动态原型模式: 由于上面的组合模式将构造函数和原型独立开来,可能会造成困扰。而动态原型模式就把这两个都封装到了构造函数中,也保持了混合模式的优点。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
      this.friend = ["A", "B"];
      // 共享方法放到条件语句中。注意不能用对象字面量来重写原型了。
      // 在已经创建实例的情况下直接重写会完全斩断已有实例和新原型的联系,无法保证改动即时反映到所有实例中。
      if (typeof this.sayName != "function") {
      Person.prototype.sayName = function() {
      alert(this.name);
      };
      }
      }
    • 寄生构造函数模式: 可用于在特殊情况下为对象创建构造函数,例如创建一个具有特殊方法的数组。其实和工厂模式没什么区别。所以无法用instanceof操作符确定对象的类型。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function SpecialArray() {
      var values = new Array();
      values.push.apply(values, arguments); // 将传入的值添加进去
      values.toPipedString = function() {
      return this.join("|");
      }
      return values; // 和工厂模式一样有返回
      }
      // 和工厂模式不同的是要用new操作符
      var colors = new SpecialArray("red", "blue", "green");
    • 稳妥构造函数模式: 稳妥durable指的是没有公共属性、且其方法也不引用this、不使用new操作符调用构造函数。

  3. 继承

    • 传统的继承: 通常OO语言支持两种继承方式,即接口继承实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。
    • ECMAScript中的继承: 由于函数没有签名,故JavaScript中没有接口继承,只有实现继承。实现方式主要是通过原型链。
    • 原型链: 利用原型让一个引用类型继承另一个引用类型的属性和方法。不妨回忆一下构造函数、原型与实例之间的关系。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例则包含一个指向原型对象的内部指针。实现的本质是重写原型对象,代之以一个新类型的实例
    • 确定原型和实例之间的关系: ins instanceof Object使用instance操作符测试实例与构造函数,只要构造函数在它的原型链中出现过就为true.还可以使用Object.prototype.isPrototypeOf(ins)判断该实例是否是该原型链派生。
    • 子类重写超类型中的方法: 必须在用父类new出来的实例替换掉子类的prototype后再重新定义方法。而且只能通过name.prototype.methodName = function(){}的形式来重写,不能用对象字面量的方式。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      function SuperType() {
      this.property = true;
      }
      SuperType.prototype.getSuperValue = function() {
      return this.property;
      };
      function SubType() {
      this.subproperty = false;
      }
      SubType.prototype = new SuperType();
      SubType.prototype.getSubValue = function() {
      return this.subproperty;
      };
      SubType.prototype.getSuperValue = function() {
      return false;
      };
      var instance = new SubType();
      alert(instance.getSuperValue()); // 弹出false;
    • 原型链的问题:
      (1) 超类型实例中的引用类型会为所有子类共享,子类实例对其修改是全局可见的;
      (2) 在创建子类型的实例时,不能向超类型的构造函数中传递参数。

    • 借用构造函数constructor stealing: 在子类型构造函数的内部调用超类型的构造函数。毕竟函数只不过是在特定环境中执行代码的对象,因此通过apply或call也可以在新创建对象上执行构造函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function SuperType(countryName) {
      this.flagColors = ["red", "yellow"];
      this.countryName = countryName;
      }
      function SubType() {
      SuperType.call(this, "China");
      this.age = 67;
      }
      var instance = new SubType();
    • 借用构造函数的问题: 无法避免构造函数本身的问题,即每个实例都会创建一个方法副本,函数复用根本无从谈起。

    • 组合继承: 将原型链和借用构造函数组合起来,使用原型链实现对原型属性方法的继承,使用借用构造函数实现对实例属性的继承。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      function SuperType(countryName) {
      this.flagColors = ["red", "yellow"];
      this.countryName = countryName;
      }
      function SubType(countryName, age) {
      SuperType.call(this, countryName); // 借用构造函数。第二次调用超类型构造函数。
      this.age = 67;
      }
      SubType.prototype = new SuperType(); // 第一次调用超类型构造函数。
      SubType.prototype.sayAge = function() { // 原型链
      alert(this.age);
      };
      var instance = new SubType();
    • 组合继承的问题: 无论什么情况下,都会调用两次超类型构造函数: 一次在创建子类型原型的时候,另一次是在子类型构造函数内部。

    • 原型式继承prototypal inheritance: 可手写一个object函数或使用ECMAScript 5定义的Object.create函数实现继承,其中Object.create()除了接受原型对象实例,还可以传入额外的属性对象。但用原型式继承的方式,超类型实例中的引用类型属性又是共享的了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
      }
      var person = {
      name: "Bobby",
      friends: ["A", "B"]
      };
      var anotherPerson = object(person);
      var yetAnotherPerson = Object.create(person, {
      name: {
      value: "C"
      }
      });
    • 寄生式继承: 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function createAnother(original) {
      var clone = object(original);
      clone.sayHi = function() { // 增强对象,也就是加点料
      alert("hi");
      };
      return clone;
      }
      var anotherPerson = createAnother(person);
    • 寄生组合式继承: 首先要指出,这是引用类型最理想的继承范式。前面提到组合继承需要调用两次超类型的构造函数,所以这里的思路是不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。我们可以使用寄生式继承来继承超类型的原型,然后将结果指定给子类型的原型。它的高效性体现在只调用一次超类型的构造函数、避免了创建不必要的冗余属性。它仍保持了原型链,故instanceof和isPrototypeOf都能正常使用。

      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
      30
      31
      function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
      }
      function inheritPrototype(subType, superType) {
      var prototype = object(superType.prototype);// 创建超类型原型的一个副本
      prototype.constructor = subType; // 弥补因重写原型而失去的默认的constructor属性
      subType.prototype = prototype; // 将副本赋值作为子类型的原型
      }
      function SuperType(name) {
      this.name = name;
      this.colors = ["red", "yellow"];
      }
      SuperType.prototype.sayName = function() {
      alert(this.name);
      };
      function SubType(name, age) {
      SuperType.call(this, name);
      this.age = age;
      }
      inheritPrototype(SubType, SuperType);
      SubType.prototype.sayAge = function() {
      alert(this.age);
      };

函数表达式


  1. ECMAScript中的函数
    如前面所提,函数声明有两种方式。一是正常的函数声明,二是函数表达式(赋值)。前者具有函数提升的特性,保证在执行代码之前,所声明的函数都是可用的。
  2. 递归
    如前面所提,函数声明的形式实现递归,可能导致无法正确引用原函数的问题。一个解决方法是使用arguments.callee,但在strict模式下不能用。所以最好用函数表达式+重命名内部函数来实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var factorial = (function f(n) {
    if (n <= 1) {
    return 1;
    } else {
    return n * f(n - 1);
    }
    });
    var anotherFac = factorial;
    factorial = null;
    alert(anotherFac(4));
  3. 闭包

    • 闭包的概念: 指有权访问另一个函数作用域中变量的函数。创建闭包的常见形式,就是在一个函数内部创建另一个函数。当函数返回了一个闭包时,这个函数的作用域将会一直存在于内存直到闭包不存在为止。
    • 以下面这个“创建比较函数”的代码为例。在createCmp内部创建的匿名函数中访问了外部的变量propertyName,即使内部函数已经被返回、离开了createCmp的执行环境、且创建的内部函数是在其他地方被调用,也仍然可以访问到createCmp的变量。这是因为在函数内部定义的函数会将包含它的函数(即外部函数)的活动对象添加到它的作用域链中,当createCmp函数返回后,它的活动对象不会被销毁,因为它包含的匿名函数的作用域链仍在引用它的活动对象。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      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;
      }
      };
      }
      var cmpNames = createCmp("names");
      var result = cmpNames({name: "A"}, {name: "B"});
      cmpNames = null; // 匿名函数在此时才销毁,手动通知释放内存
    • 谨慎使用闭包: 由于闭包会携带包含它的函数的作用域,所以会比正常的函数占用更多内存。

    • 闭包与变量: 闭包只能取得外部包含它的函数的任何变量的最终值。这是由于闭包保存的是外部活动对象这个整体,而不是具体的某个变量。在下面的例1中,每个匿名函数都保存着同一个createFunctions函数的活动对象,它们引用的是同一个i,也是最终状态的i。而例2则创建多了一个匿名函数并立即执行,将结果返回给数组

      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
      // 例1
      function createFunctions() {
      var result = new Array();
      for (var i = 0; i < 10; i++) {
      result[i] = function() {
      return i;
      };
      }
      return result; // 全都是10
      }
      // 例2
      function createFunctions() {
      var result = new Array();
      for (var i = 0; i < 10; i++) {
      result[i] = function(num) {
      return function() {
      return num; // 访问num的闭包
      };
      }(i); // 立即执行匿名函数
      }
      return result; // 1, 2, 3, ..., 10
      }
    • 闭包中的this: this对象是在运行时基于函数的执行环境而定的。由于匿名函数的执行环境具有全局性,其中的this对象通常指向window.每个函数在调用时,其活动对象都会自动取得this和arguments两个特殊变量,内部函数在搜索这两个变量时,只会搜索到它自己的活动变量为止,不可能直接访问外部函数的这两个变量。如下面的例子所示,调用可写作(object.(sayName())()),先执行里面一层,再执行后面一层的匿名函数。既然闭包能够访问的是在包含函数中特意用var声明的变量,那我们就可以先用一个that变量将包含函数的this保存起来,这样即使调用时第一层的函数返回了,that也保持着对object的引用,这样匿名函数就能正常访问到object内定义的name了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      var name = "The Window";
      var object = {
      name: "The Object",
      sayName: function() {
      return function() {
      return this.name;
      };
      }
      };
      alert(object.sayName()()); // 在非strict模式下弹出The Window
      // 用闭包让匿名函数能访问到外部函数的this
      var object = {
      name: "The Object",
      sayName: function() {
      var that = this;
      return function() {
      return that.name;
      };
      }
      };
      alert(object.sayName()()); // 弹出The Object
    • 防止内存泄漏: 如果闭包的作用域链中保存着一个HTML元素,该元素无法正常销毁。如例1中为element的onclick事件创建了处理程序的闭包,而匿名函数又保持了对assignHandler函数中活动对象的引用,这导致无法减少element的引用数而无法被回收。所以我们需要养成好习惯,将DOM元素的某些属性单独拎出来存入单独的变量,然后在匿名函数后面手动将DOM操作的元素变量赋值为null解除对DOM元素的引用。如果没有这一步,即使闭包不直接引用element,闭包引用的包含函数的整个活动对象仍会保存着element的引用。

      1
      2
      3
      4
      5
      6
      function assignHandler() {
      var element = document.getElementById("someElement");
      element.onclick = function() {
      alert(element.id);
      }
      }
  4. 用匿名函数模仿块级作用域

    • 块级作用域: 在JavaScript中是没有块级作用域的,在块语句中定义的变量,实际上在外部依然可以访问,只要作用域没有销毁。

      1
      2
      3
      4
      5
      6
      function outputN(n) {
      for (var i = 0; i < count; i++) {
      alert(i);
      }
      alert(i); // 仍然可以访问
      }
    • 匿名函数cosplay块级作用域: 在匿名函数内定义的任何变量,都会在执行结束时销毁。那么我们可以在声明一个匿名函数后立即执行,则其中的变量就会自然而然地销毁了。这个特性可以在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。

      1
      2
      3
      4
      5
      6
      (function() {
      var now = new Date();
      if (now.getMonth() == 0 && now.getDate() == 1) {
      alert("Happy new year!");
      }
      })(); // 对应传入参数,立即执行
  5. 私有变量

    • 私有变量的概念: 任何在函数中定义的变量都是私有变量,因为不能在函数的外部访问到这些变量。私有变量包括函数的参数、局部变量和函数内部定义的其他函数。(注意说的不是私有属性!JavaScript的所有属性都是公有的!)
    • 特权方法privileged method: 有权访问私有变量和函数的公有方法。利用私有和特权成员,可以隐藏那些不应该被直接修改的数据。在构造函数中,在定义方法前加this即可,利用的是“初始化未经声明的变量,总会创建一个全局变量”。但缺点是每个实例都会创建同样的一组方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function MyObject() {
      var privateVar = 10;
      function privateFunc() {
      return false;
      }
      // 特权方法。但在strict模式下给未经var声明的变量赋值会报错
      this.publicMethod = function() {
      privateVar++;
      return privateFunc();
      };
      }
    • 静态私有变量: 私有变量和函数由实例共享,变量的修改牵一发而动全身。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      (function() {
      var name = "";
      Person = function(value) {
      name = value;
      };
      Person.prototype.getName = function() {
      return name;
      };
      Person.prototype.setName = function(value) {
      name = value;
      };
      }) ();
      var p1 = new Person("A");
      var p2 = new Person("B");
      alert(p1.getName()); // B
      alert(p2.getName()); // B
    • 模块模式module pattern: 为单例singleton创建私有变量和特权的方法。单例指的是只有一个实例的对象,JavaScript中的对象字面量可以很方便地创建单例对象。如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,就可以用模块模式。注意每个单例的类型用instanceof都只能得出是Object类型,作为全局变量存在。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      var application = function() {
      // 私有变量和函数
      var components = new Array();
      components.push(new BaseComponent()); // 初始化,例如丢进一些组件
      // 公共
      return {
      getComponentCount: function() {
      return components.length;
      },
      registerComponent: function(component) {
      if (typeof component == "object") {
      components.push(component);
      }
      }
      };
      } ();
    • 增强版模块模式: 在返回对象之前加入对其增强的代码,即返回的是一个特定类型的对象实例同时对其添加了属性和方法进行增强。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      var application = function() {
      // 私有变量和函数
      var components = new Array();
      components.push(new BaseComponent()); // 初始化,例如丢进一些组件
      // 创建指定类型的局部副本
      var app = new BaseComponent();
      // 公共接口
      app.getComponentCount = function() {
      return components.length;
      };
      app.registerComponent = function(component) {
      if (typeof component == "object") {
      components.push(component);
      }
      };
      return app; // 将特定类型的实例返回
      } ();

BOM


  1. window对象

    • BOM: 浏览器对象模型,是JavaScript能在Web中使用的关键。BOM提供了很多对象,用于访问浏览器的功能,而与页面无关。
    • window: BOM的核心对象,是浏览器的实例。它既是通过JavaScript访问浏览器窗口的一个接口、又是ECMAScript规定的Global对象。
    • 全局作用域: 所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法。不过直接定义到window上的属性和全局变量还是有一点区别的,前者可以用delete操作符从window中删除,后者不行。window还能提供一个”属性查询”的功能:

      1
      2
      3
      var newValue = oldValue; // 若oldValue没有定义,会报错
      var newValue = window.oldValue; //这是一次属性查询,newValue获得值undefined
    • frame中的window: 在HTML中使用frameset+frame可以在同一个浏览器窗口中显示多个独立的小方框显示不同的页面。虽然现在已经很少用frame了,不过不妨了解一下在使用frame的情况下如何获取各个独立页面的window对象。每个frame都有独立的window对象,存储在frames集合中,可通过索引或名称的方式访问,即top.frames[0]top.frames["topFrame"]。其中top对象始终指向最外层框架,即浏览器窗口,而如果用window对象访问frames集合,可能指向的是某个框架的特定实例而不是顶层的窗口。与top对象相对应地,parent对象始终指向当前框架的上层

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <html>
      <head>
      <title>Frameset Example</title>
      </head>
      <body>
      <frameset rows="100,*">
      <frame src="frame.html" name="topFrame"></frame>
      <frameset cols="50%, 50%">
      <frame src="anotherFrame.html" name="leftFrame"></frame>
      <frame src="anotherFrameset.html" name="rightFrame"></frame>
      </frameset>
      </frameset>
      </body>
      </html>
      其中的anotherFrameset.html包含一个新的frameset,这里就不列出来了。
    • 窗口位置: 使用screenLeft和screenTop(或screenX和screenY)可取得窗口的位置信息,但可能无法跨浏览器获得窗口左边和上边的精确坐标,而且可能无法获取frame的坐标。而移动窗口到指定位置可以用window.moveTo和window.moveBy完成,不过可能被浏览器默认禁用。

      1
      2
      3
      4
      5
      6
      // 跨浏览器获取页面左边和上边的位置
      var leftPos = (typeof window.screenLeft == "number")? window.screenLeft: window.screenX;
      var topPos = (typeof window.screenTop == "number")? window.screenTop: window.screenY;
      window.moveTo(0, 0); // 移动窗口到左上角
      window.moveBy(-50, 100); // 左移50,下移100
    • 窗口大小: 使用innerWidth, innerHeight, outerWidth, outerHeight可以获取页面视图大小和外层浏览器本身的尺寸,但外层尺寸不一定可以跨浏览器获取。此外,还可以通过document属性的方式来获取。调整窗口可以使用window.resizeTo和window.resizeBy,也可能会被浏览器禁用。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // document方式获取窗口大小
      var pageWidth = window.innerWidth,
      pageHeight = window.innerHeight;
      if (typeof pageWidth !== "number") {
      if (document.compatMode === "CSS1Compat") {
      pageWidth = document.documentElement.clientWidth;
      pageHeight = document.documentElement.clientHeight;
      } else {
      pageWidth = document.body.clientWidth;
      pageHeight = document.body.clientHeight;
      }
      }
      // 调整到100 * 100
      window.resizeTo(100, 100);
      // 调整到200 * 50
      window.resizeBy(100, -50);
    • 打开窗口: 使用window.open可以导航到特定URL或打开一个新的浏览器窗口,接收4个参数:目标URL、target、特性字符串和是否取代当前页历史记录的boolean。其中第二个参数会优先查找并打开在URL中是否有该名称的窗口或框架,其次可以选择_self, _parent, _top, 或_blank,最后若确实没有找到该名称,则会根据第三个参数打开新窗口。window.open会返回一个指向新窗口的引用,可存入一个变量中对该窗口进行其他操作。新窗口还会用opener指向原始窗口,但原窗口并不会记录自己打开了哪些新窗口。有些浏览器会有弹出窗口屏蔽的功能,我们有时需要检测弹出的窗口是否被封杀。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      window.open("http://xxx.com", "newWindow", "height=400, width=400, top=20, left=0, resizeable=yes");
      // 检测封杀
      var blocked = false;
      try {
      var newWin = window.open("http://xxx.com", "_blank");
      if (newWin === null) {
      blocked = true;
      }
      } catch (ex) { // 有些封杀后会抛出异常
      blocked = true;
      }
    • 超时调用与间歇调用: setTimeout(function, timems)可控制延时多少ms再执行函数,可利用返回的id结合clearTimeout取消掉。setInterval(function, timems)则是设置每间隔多少ms就执行一次函数。在真正开发时,间歇调用很少用上,因为后一个间歇调用很可能在前一个间歇调用结束之前启动。完全可以用超时调用来模拟间歇调用避免间歇调用的混乱。

      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
      var timeoutId = setTimeout(function() {
      alert("Hey, girl!")
      }, 1000);
      clearTimeout(timeoutId);
      var tick = 0, max = 10;
      var intervalId = null;
      function tickIncre() {
      tick++;
      if (tick === max) {
      clearInterval(intervalId);
      alert("tick Done!");
      }
      }
      intervalId = setInterval(tickIncre, 500);
      // 用超时调用模拟间歇调用
      var tick = 0, max = 10;
      var intervalId = null;
      function tickIncre() {
      tick++;
      if (tick < max) {
      setTimeout(tickIncre, 500);
      } else {
      alert("tick Done!");
      }
      }
      setTimeout(tickIncre, 500);
    • 浏览器对话框: 对话框样式由浏览器定义,与网页无关。打开的对话框都是同步和模态的,弹出时代码会暂停执行,等用户操作反馈后才会恢复执行。alert()弹出只有“确定”按钮的对话框;confirm()则有“确定”和“取消”两个按钮,会根据用户的选择返回布尔值;prompt()与confirm相比又多了一个文本输入框,点击确定则会返回这个字符串、否则返回null。

      1
      2
      3
      4
      var result = prompt("What do you mean?", "hint text");
      if (result !== null) {
      alert("Welcome, " + result);
      }
  2. location对象
    location对象既是window的属性,又是document的属性。location保存着当前文档的信息,还将URL分割成独立的片段,可通过不同属性访问这些片段。

    • 查询参数: 在URL后接参数(以?开头)可以传递参数,使用location.search可以访问这一串参数,但是没法把每个参数拆分开来。不妨手动实现一发,直接用返回对象的属性的形式来访问各个参数,十分方便。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      function getQueryStringArgs() {
      var qs = (location.search.length > 0 ? location.search.substring(1) : "",
      args = {},
      items = qs.length ? qs.split("&") : [];
      item = null,
      name = null,
      value = null;
      for (var i = 0, len = items.length; i < len; i++) {
      item = items[i].split("=");
      name = decodeURIComponent(item[0]);
      value = decodeURIComponent(item[1]);
      if (name.length) {
      args[name] = value;
      }
      }
      return args;
      }
    • 地址跳转: 使用assign方法、设置window.location、修改location.href属性是三种等价的方式可以进行地址跳转。而使用replace方法就是直接替换地址(即无法后退)。使用reload方法可以重新加载页面。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 浏览器地址跳转
      location.assign("http://xxx.com");
      window.location = "http://xxx.com";
      location.href = "http://xxx.com";
      // 替换
      location.replace("http://xxx.com");
      // 重加载
      location.reload(); // 重新加载,可能用到缓存
      location.reload(true); // 从服务器重新加载
  3. navigator对象
    用于识别客户端浏览器,拥有一大堆属性帮助检测浏览器。

    • 检测插件: 对于非IE浏览器可通过navigator.plugins来获取插件的集合,然后进行字符串搜索就可以知道是否安装给定的插件。而IE得使用ActiveXObject尝试创建一个实例来检测是否有给定的插件。通常会针对不同插件编写不同的函数,保证跨浏览器都能检测。

      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
      function hasPlugin(name) {
      name = name.toLowerCase();
      for (var i = 0, len = navigator.plugins.length; i < len; i++) {
      if (navigator.plugins[i].name.toLowerCase().indexOf(name) > -1) {
      return true;
      }
      }
      return false;
      }
      // IE专属
      function hasIEPlugin(name) {
      try {
      new ActiveXObject(name);
      return true;
      } catch (ex) {
      return false;
      }
      }
      function hasQuickTime() {
      var result = hasPlugin("QuickTime");
      if (!result) {
      result = hasIEPlugin("QuickTime.QuickTime");
      }
      return result;
      }
    • 注册处理程序: navigator.registerContentHandler()navigator.registerProtocolHandler()可以让站点指明它所能处理特定类型的信息。使用history.length还可以得知当前窗口的记录数量,可用于判断本页面是否为用户打开的第一个页面。

  4. screen对象
    包含浏览器外部显示屏的信息,例如availWidth, availHeight。
  5. history对象
    包含用户的上网记录,每个窗口都有history属性。虽然不能获取用户的历史记录,但可以用go函数实现前进、后退等跳转。
    1
    2
    3
    4
    5
    history.go(2); // 前进2页
    history.go(-1); // 后退1页
    history.back();
    history.forward();