《JavaScript高级程序设计》学习笔记

第 1 章 JavaScript 简介

1.1 JavaScript 简史

1.2 JavaScript 实现

1.2.1 ECMAScript

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 操作符
  • 对象

1.2.2 文档对象模型 DOM

1.2.3 浏览器对象模型 BOM

1.3 JavaScript 版本

第 2 章 在 HTML 中使用 JavaScript

2.1 script 元素

  • async:立即下载脚本,不妨碍其他操作
  • charset
  • defer:脚本可以延迟到文档完全被解析和显示后再执行
  • language:已废弃
  • src
  • type:默认为 text/javascript

2.1.1 标签的位置

浏览器会等待 JavaScript 下载、解析和执行,因此一般把全部 JavaScript 引用放在<body>元素中页面内容的后面

2.1.2 延迟脚本

使用 defer 属性可以让脚本在文档完全呈现之后再执行。延迟脚本总是按照指定它们的顺序执行。

2.1.3 异步脚本

使用 async 属性可以表示当前脚本不必等待其他脚本,也不必阻塞文档呈现。不能保证异步脚本按照它们在页面中出现的顺序执行。

2.1.4 在 XHTML 中的用法(略)

2.1.5 不推荐使用的语法(略)

2.2 嵌入代码与外部文件

2.3 文档模式

2.5 noscript 元素

第 3 章 基本概念

3.1 语法

3.1.1 区分大小写

3.1.2 标识符

所谓标识符,就是指变量、函数、属性的名字,或者函数的参数。

3.1.3 注释

3.1.4 严格模式

3.1.5 语句

3.2 关键字和保留字

3.3 变量

3.4 数据类型

  • 5 种简单数据类型 Undefined, Null, Boolean, Number, String
  • 1 种复杂数据类型 Object

3.4.1 typeof 操作符

  • "undefined"——如果这个值未定义;
  • "boolean"——如果这个值是布尔值;
  • "string"——如果这个值是字符串;
  • "number"——如果这个值是数值;
  • "object"——如果这个值是对象或 null;
  • "function"——如果这个值是函数。

3.4.2 Undefined 类型

Undefined 类型只有一个值,即特殊的 undefined。使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined。

3.4.3 Null 类型

Null 类型是第二个只有一个值的数据类型,这个特殊的值是 null。从逻辑角度来看,null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回"object"的原因。

实际上,undefined 值是派生自 null 值的,因此 ECMA-262 规定对它们的相等性测试要返回 true。

alert(null == undefined) // true

无论在什么情况下都没有必要把一个变量的值显式地设置为 undefined,但是只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存 null 值。

3.4.4 Boolean 类型

Boolean 类型是 ECMAScript 中使用得最多的一种类型,该类型只有两个字面值:true 和 false。

3.4.5 Number 类型

Number()、parseInt()、parseFloat()

由于 Number()函数在转换字符串时比较复杂而且不够合理,因此在处理整数的时候更常用的是 parseInt()函数。

用 parseInt()转换空字符串会返回 NaN(Number()对空字符返回 0)

3.4.6 String 类型

数值、布尔值、对象和字符串值(没错,每个字符串也都有一个 toString()方法,该方法返回字符串的一个副本)都有 toString()方法。但 null 和 undefined 值没有这个方法。

3.4.7 Object 类型

ECMAScript 中的对象其实就是一组数据和功能的集合。

Object 的每个实例都具有下列属性和方法。

  • constructor:保存着用于创建当前对象的函数。对于前面的例子而言,构造函数(constructor)就是 Object()。
  • hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。其中,作为参数的属性名(propertyName)必须以字符串形式指定(例如:o.hasOwnProperty("name"))。
  • isPrototypeOf(object):用于检查传入的对象是否是当前对象的原型(第 5 章将讨论原型)。
  • propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用 for-in 语句(本章后面将会讨论)来枚举。与 hasOwnProperty()方法一样,作为参数的属性名必须以字符串形式指定。
  • toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString()方法的返回值相同。

3.5 操作符

3.6 语句

3.7 函数

第 4 章 变量、作用域和内存问题

4.1 基本类型和引用类型的值

基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。

4.1.1 动态的属性

对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法。

4.1.2 复制变量值

复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。

image

复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量。

image

4.1.3 传递参数

function setName(o) {
  o.name = "mike";
  o = new Object();
  o.name = "mary";
}
let setNameO = new Object();
setName(setNameO);
console.log(setNameO);

4.1.4 检测类型

typeof 的局限性:只能检测基本数据类型

如果变量是给定引用类型(根据它的原型链来识别;第 6 章将介绍原型链)的实例,那么 instanceof 操作符就会返回 true:

person instanceof Object
person instanceof Array
person instanceof RegExp

所有引用类型的值都是 Object 的实例。因此,在检测一个引用类型值和 Object 构造函数时,instanceof 操作符始终会返回 true。

如果使用 instanceof 操作符检测基本类型的值,则该操作符始终会返回 false,因为基本类型不是对象。

4.2 执行环境及作用域

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

每个函数都有自己的执行环境。当代码在一个环境中执行时,会创建变量对象的一个作用域链。

作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

4.2.1 延长作用域链

在作用域链的前端添加一个变量对象,该变量对象会在代码执行后被移除,当执行流进入下列任何一个语句时,作用域链就会得到加长:

  • try-catch 语句的 catch 块
  • with 语句

对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

4.2.2 没有块级作用域

对于 JavaScript 来说,由 for 语句创建的变量 i 即使在 for 循环执行结束后,也依旧会存在于循环外部的执行环境中。

4.3 垃圾收集

JavaScript 具有自动垃圾收集机制,这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。

下面我们来分析一下函数中局部变量的正常生命周期。局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。

垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。

  • 标记清除
  • 引用计数

4.4 小结

基本类型值和引用类型值具有以下特点:

  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;
  • 引用类型的值是对象,保存在堆内存中;
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
  • 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;
  • 确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用 instanceof 操作符。

所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。以下是关于执行环境的几点总结:

  • 执行环境有全局执行环境(也称为全局环境)和函数执行环境之分;
  • 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;
  • 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境;
  • 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据;
  • 变量的执行环境有助于确定应该何时释放内存。

第 5 章 引用类型

5.1 Object 类型

创建 Object 实例,第一种是使用 new 操作符后跟 Object 构造函数,另一种方式是使用对象字面量表示法。

5.2 Array 类型

创建数组的基本方式有两种。第一种是使用 Array 构造函数,第二种是使用数组字面量表示法.

与对象一样,在使用数组字面量表示法时,也不会调用 Array 构造函数(Firefox 3 及更早版本除外)。

5.2.1 检测数组

  • instanceof // instanceof 操作符的问题在于,它假定只有一个全局执行环境
  • Array.isArray()

5.2.2 转换方法

数组继承的 toLocaleString()、toString()和 valueOf()方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。

join()方法,则可以使用不同的分隔符来构建这个字符串。

如果数组中的某一项的值是 null 或者 undefined,那么该值在 join()、toLocaleString()、toString()和 valueOf()方法返回的结果中以空字符串表示。

5.2.3 栈方法

  • push(): 可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。
  • pop(): 从数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项。

5.2.4 队列方法

  • shift(): 从数组前端取得项,同时将数组长度减 1
  • unshift(): 在数组前端添加任意个项并返回新数组的长度

5.2.5 重排序方法

  • reverse(): 反转数组项的顺序
  • sort(): 调用每个数组项的 toString()转型方法,然后比较得到的字符串,以确定如何排序。

5.2.6 操作方法

  • concat():基于当前数组中的所有项创建一个新数组
  • slice():基于当前数组中的一或多个项创建一个新数组
  • splice():主要用途是向数组的中部插入项

5.2.7 位置方法

  • indexOf()
  • lastIndexOf()

5.2.8 迭代方法

  • every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true。
  • filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。
  • forEach():对数组中的每一项运行给定函数。这个方法没有返回值。
  • map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
  • some():对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。

5.2.9 归并方法

  • reduce()
  • reduceRight()

5.3 Date 类型

5.4 RegExp 类型

5.5 Function 类型

每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。即,函数是对象,函数名是指针。

5.5.1 没有重载(深入理解)

5.5.2 函数声明与函数表达式

函数声明:解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);

function sum (num1, num2){
  return num1 + num2
}

函数表达式:至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

var sum = function (num1, num2) {
  return num1 + num2
}

5.5.3 作为值的函数

不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

function createComparisonFunction(property) {
  return function (obj1, obj2) {
    let value1 = obj1[property],
      value1 = obj2[property];
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}
let compareData = [
  {
    name: "mike",
    age: 20,
  },
  {
    name: "mary",
    age: 18,
  },
];
compareData.sort(createComparisonFunction("name"));

5.5.4 函数内部属性

在函数内部,有两个特殊的对象:arguments 和 this。

  • arguments:它是一个类数组对象,包含着传入函数中的所有参数,它有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
  • this:this 引用的是函数执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时,this 对象引用的就是 window)

5.5.5 函数属性和方法

每个函数都包含两个属性:length 和 prototype。

其中,length 属性表示函数希望接收的命名参数的个数。

对于 ECMAScript 中的引用类型而言,prototype 是保存它们所有实例方法的真正所在。

每个函数都包含两个非继承而来的方法:apply()和 call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。

apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。call()方法与 apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。在使用 call()方法时,传递给函数的参数必须逐个列举出来。

ECMAScript 5 还定义了一个方法:bind()。这个方法会创建一个函数的实例,其 this 值会被绑定到传给 bind()函数的值。

window.color = 'red';
var o = {
  color: 'blue';
}
function sayColor () {
  alert(this.color);
}
sayColor(); // red
sayColor.call(o); // blue
var sayColorObject = sayColor.bind(o);
sayColorObject(); // blue

每个函数继承的 toLocaleString(),toString()和 valueOf()方法始终都返回函数的代码。

5.6 基本包装类型

为了便于操作基本类型值,ECMAScript 还提供了 3 个特殊的引用类型:Boolean、Number 和 String

引用类型与基本包装类型的主要区别就是对象的生存期。使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。

5.6.1 Boolean 类型

Boolean 类型的实例重写了 valueOf()方法,返回基本类型值 true 或 false;重写了 toString()方法,返回字符串"true"和"false"。

var booleanObject = new Boolean(true);
var booleanValue = false;
alert(typeof booleanObject); // object
alert(typeof booleanValue); // boolean
alert(booleanObject instanceof Boolean) // true
alert(booleanObject instanceof Boolean) // false

5.6.2 Number 类型

Number 类型也重写了 valueOf()、toLocaleString()和 toString()方法。重写后的 valueOf()方法返回对象表示的基本类型的数值,另外两个方法则返回字符串形式的数值。

toFixed()方法会按照指定的小数位返回数值的字符串表示。

5.6.3 String 类型

String 对象的方法也可以在所有基本的字符串值中访问到。其中,继承的 valueOf()、toLocale-String()和 toString()方法,都返回对象所表示的基本字符串值。

String 类型提供了很多方法,用于辅助完成对 ECMAScript 中字符串的解析和操作。

  • 字符方法:charAt()和 charCodeAt()。
  • 字符串操作方法
    • concat()
    • slice()
    • substr()
    • substring()
  • 字符串位置方法:indexOf()和 lastIndexOf()
  • trim()方法
  • 字符串大小写转换方法
  • 字符串的模式匹配方法
    • match()
    • search()
    • replace()
    • split()

5.7 单体内置对象

5.7.1 Global 对象

5.7.2 Math 对象

第 6 章 面向对象的程序设计

6.1 理解对象

6.1.1 属性类型

ECMAScript 中有两种属性:数据属性和访问器属性。

  • 数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。

  1. [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
  2. [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
  3. [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
  4. [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined。
  • 访问器属性

访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。

  1. [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为 true。
  2. [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为 true。❏、
  3. [[Get]]:在读取属性时调用的函数。默认值为 undefined。
  4. [[Set]]:在写入属性时调用的函数。默认值为 undefined。

访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。

var book = {
  _year: 2001,
  edition: 1
};
Object.defineProperty('book', 'year', {
  get: function(){
    return this._year;
  }
  set: function(newValue){
    if (newValue > 2001) {
      this._year = newValue
      this.edition += newValue - 2001
    }
  }
})
book.year = 2002
alert(book.edition) // 2

6.1.2 定义多个属性

Object.definePro-perties():利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

var book = {};
Object.definePro-perties(book, {
  _year: {
    writable: true,
    value: 2004
  },
  edition: {
    writable: true,
    value: 1
  },
  year: {
    get: function(){
      return this._year;
    }
    set: function(newValue){
      if (newValue > 2001) {
        this._year = newValue
        this.edition += newValue - 2001
      }
    }
  }
})

6.1.3 读取属性的特性

使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这个对象的属性有 configurable、enumerable、writable 和 value。

6.2 创建对象

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

6.2.1 工厂模式

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('Mike', 20, 'engineer');
var person2 = createPerson('Mary', 21, 'doctor');

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

6.2.2 构造函数模式

创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    alert(this.name);
  };
}
var person1 = new Person('Mike', 20, 'engineer');
var person2 = new Person('Mary', 21, 'doctor');

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:

(1) 创建一个新对象;

(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);

(3) 执行构造函数中的代码(为这个新对象添加属性);

(4) 返回新对象。

构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

6.2.3 原型模式

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

function Person() {};
Person.prototype.name = 'Mike';
Person.prototype.age = 20;
Person.sayName = function() {
  alert(this.name);
};
var person1 = new Person();
person1.sayName; // Mike
var person2 = new Person();
person1.sayName == person2.sayName; // true

与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName()函数。

  1. 理解原型对象

    image

    每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

    使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。

  2. 原型与 in 操作符

    同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示:

    function(object, name) {
      return !object.hasOwnProperty(name) && (name in object);
    }
    

    由于 in 操作符只要通过对象能够访问到属性就返回 true, hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确定属性是原型中的属性。

  3. 更简单的原型语法

    function Person() {};
    Person.prototype = {
      name: 'Mike',
      age: 20,
      job: 'engineer',
      sayName: function() {
        alert(this.name)
      }
    };
    

    在上面的代码中,我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor 属性不再指向 Person 了,我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。此时,尽管 instanceof 操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了,如下所示。

    var friend = new Person();
    alert(friend instanceof Object); // true
    alert(friend instanceof Person); // true
    alert(friend.constructor == Person); // false
    alert(friend.constructor == Object); // true
    

    如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。

    function Person() {};
    Person.prototype = {
      constructor: Person,
      name: 'Mike',
      age: 20,
      job: 'engineer',
      sayName: function() {
        alert(this.name)
      }
    };
    
  4. 原型的动态性

    由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。请看下面的例子。

    function Person() {};
    var friend = new Person;
    Person.prototype.sayHi = function() {
      alert('hi');
    };
    friend.sayHi(); // alert hi
    

    这是因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回保存在那里的函数。

    尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。

    function Person() {};
    var friend = new Person();
    Person.prototype = {
      constructor: Person,
      name: 'Mike',
      age: 20,
      job: 'engineer',
      sayName: function() {
        alert(this.name)
      }
    };
    friend.sayName(); // error
    
    image

    从图中可以看出,重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

  5. 原生对象的原型

    所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。

  6. 原型对象的问题

    首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。其次,对于包含引用类型值的属性来说,问题就比较突出了。

6.2.4 组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['Tommy', 'Lily'];
}
Person.prototype = {
  constructor: Person,
  sayName: function() {
    alert(this.name);
  };
};
var person1 = new Person('Mike', 20, 'engineer');
var person2 = new Person('Mary', 21, 'teacher');
person1.friends.push('Bob');
alert(person1.friends); // 'Tommy, Lily, Bob'
alert(person2.friends); // 'Tommy, Lily'
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true

6.2.5 动态原型模式

6.2.6 寄生构造函数模式

6.2.7 稳妥构造函数模式

6.3 继承

ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

6.3.1 原型链

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
}
function SubType() {
  this.subValue = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
  return this.subValue;
}
var instance = new SubType();
instance.getSuperValue(); // true

最终结果就是这样的:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。getSuperValue()方法仍然还在 SuperType.prototype 中,但 property 则位于 SubType.prototype 中。这是因为 property 是一个实例属性,而 getSuperValue()则是一个原型方法。既然 SubType.prototype 现在是 SuperType 的实例,那么 property 当然就位于该实例中了。此外,要注意 instance.constructor 现在指向的是 SuperType,这是因为原来 SubType.prototype 中的 constructor 被重写了的缘故。


image

原型链的问题:最主要的问题来自包含引用类型值的原型。

function SuperType() {
  this.colors = ['red', 'pink', 'green'];
}
function SubType() {}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors); // 'red, pink, green, black'
var instance2 = new SubType();
alert(instance2.colors); // 'red, pink, green, black'

在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。

6.3.2 借用构造函数

function SuperType() {
  this.colors = ['red', 'pink', 'green'];
}
function SubType() {
  SuperType.call(this);
}
var instance1 = new SubType();
instance1.push('black');
alert(instance1.colors); // 'red, pink, green, black'
var instance2 = new SubType();
alert(instance2.colors); // 'red, pink, green'

通过使用 call()方法(或 apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会具有自己的 colors 属性的副本了。

借用构造函数的问题:

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

6.3.3 组合继承

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'pink', 'green'];
}
SuperType.prototype.sayName = function() {
  alert(this.name);
}
function SubType(name, age) {
  // 继承属性
  SuperType.call(this, name);
  this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  alert(this.age);
}
var instance1 = new SubType('Mike', 20');
instance1.colors.push('black');
alert(instance1.colors); // 'red, pink, green, black'
instance1.sayName(); // Mike
instance1.sayAge(); // 20
var instance2 = new SubType('Mary', 21);
alert(instance2.colors); // 'red, pink, green'
instance2.sayName(); // Mary
instance2.sayAge(); // 21

6.3.4 原型式继承

6.3.5 寄生式继承

6.3.6 寄生组合式继承

第 7 章 函数表达式

7.1 递归

7.2 闭包

闭包是指有权访问另一个函数作用域中的变量的函数。

function createCompareFunction(property) {
  return function (obj1, obj2) {
    var value1 = obj1[property];
    var value2 = obj2[property];
    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}
var compare = createCompareFunction('name');
var result = compare({name: 'Mike'}, {name: 'Mary'});

当 createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

image

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

7.2.1 闭包与变量

闭包只能取得包含函数中任何变量的最后一个值。

function CreateFunctions() {
  var arr = new Array();
  for(var i = 0; i < 10; i++) {
    arr[i] = function() {
      return i;
    }
  }
  return arr;
}

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置 0 的函数返回 0,位置 1 的函数返回 1,以此类推。但实际上,每个函数都返回 10。因为每个函数的作用域链中都保存着 createFunctions()函数的活动对象,所以它们引用的都是同一个变量 i。当 createFunctions()函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是 10。但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示。

function createFunctions() {
  var arr = new Array();
  for(var i = 0; i < 10; i++) {
    arr[i] = function(num) {
      return function() {
        return num;
      };
    }(i);
  }
}

在重写了前面的 createFunctions()函数后,每个函数就会返回各自不同的索引值了。在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数 num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量 i。由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个匿名函数内部,又创建并返回了一个访问 num 的闭包。这样一来,result 数组中的每个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值了。

7.2.2 关于 this 对象

this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。

var name = 'window';
var object = {
  name: 'object',
  sayName: function() {
    return function() {
      return this.name;
    }
  }
};
alert(object.sayName()()); // window

每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量(这一点通过图 7-2 可以看得更清楚)。不过,把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示。

image
var name = 'window';
var object = {
  name: 'object',
  sayName: function() {
    var that = this;
    return function() {
      return that.name;
    }
  }
};
alert(object.sayName()()); // object

7.2.3 内存泄漏

7.3 模仿块级作用域

匿名函数可以用来模仿块级作用域。

(function() {
  // 这里是块级作用域
})()

这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容