《JavaScript高级程序设计》读书笔记(一)

Author Avatar
wshunli 10月 29, 2017
  • 在其它设备中阅读本文章

《JavaScript高级程序设计》读书笔记

从今天开始阅读 《JavaScript高级程序设计》(第三版)。

按照图灵社区推荐的阅读规划:

(一) 熟悉JavaScript的语法,理解那些JavaScript中让人疑惑的概念。(1-7章)
(二) 熟悉JavaScript运行的环境,深刻理解DOM,熟悉DOM提供给JavaScript的原生API。(8-14章)
(三) 学习HTML新增的一些对象提供的API,包括canvas ,媒体事件等。(15-16章)
(四) 学习在浏览器中进行实际开发时的错误调试技巧。(17章)
(五) 学习JavaScript对各数据载体的操作方法(如JSON、XML),学会Ajax的使用方法。(18-21章)
(六) 学习JavaScript的一些高级技巧及实践方案。(22-25章)

(一) 熟悉JavaScript的语法,理解那些JavaScript中让人疑惑的概念。(1-7章)

第一章 JavaScript 简介

一个完整的 JavaScript 实现应该由三个不同的部分组成:核心(ECMAScript)、文档对象模型(DOM)、浏览器对象模型(BOM)。

ECMAScript:提供核心语言功能;
DOM:提供访问和操作网页内容的方法和接口;
BOM:提供与浏览器交互的方法和接口。

第二章 在 HTML 中使用 JavaScript

HTML 4.01 为 <script> 元素定义了6个属性:async、charset、defer、language(废弃)、src、type 。

在不存在 defer 和 async 属性时,浏览器都会按照不同<script>元素在页面中出现的先后顺序对它们依次进行解析。

标签的位置:为了避免浏览器在呈现页面时出现明显的延迟,现代Web应用程序一般都把全部 JavaScript 引用放在<body>元素中页面内容的后面。

延迟脚本:defer 属性表明脚本在执行时不会影响页面的构造,告诉浏览器立即下载文件,但脚本会被延迟到整个页面都解析完毕后再运行;只适用于外部脚本文件。

<script defer="defer" src="example.js"></script>

异步脚本:async 属性表示当前脚本不必等待其他脚本,也不必阻塞文档呈现,告诉浏览器立即下载文件,且并不保证标记为 async 的脚本按照他们的先后顺序执行;只适用于外部脚本文件。

<script async src="example1.js"></script>
<script async src="example2.js"></script>

异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。

页面文档完全加载并解析完毕之后,会触发 DOMContentLoaded 事件,HTML 文档不会等待样式文件,图片文件,子框架页面的加载;而 load 事件可以用来检测 HTML 页面是否完全加载完毕(fully-loaded)。

第三章 基本概念

本章内容:语法、数据类型、操作符、控制流语句、函数。
其中内容和 Java 类似的部分,不再记笔记。

语法

区分大小写:ECMAScript 中的一切都区分大小写。

ECMAScript 5 引入严格模式。在整个脚本中启用严格模式,在顶部添加 "use strict";

给未经声明的变量赋值在严格模式下会导致抛出 ReferenceError 错误。

数据类型

ECMAScript 中有5种基本数据类型:Undefined、Null、Boolean、Number、String。
还有1种复杂数据类型:Object,Object 本质是一组无序的键值对组成。

typeof 操作符:用来检测变量的数据类型。
“undefined”:未定义、”boolean”:布尔值、”string”:字符串、”number”:数值、”object”:对象或 null、 “function”:函数

检测 null 值返回 “Object”、检测函数返回 “function” 。

Undefined 类型:使用 var 声明变量但未对其初始化时,这个变量的值就是 undefined。

对未声明的变量只能只能执行一项操作,即使用 tyoeof 检测其数据类型,返回 undefined 值。

Null 类型:null 值表示一个空对象指针;只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存 null 值。

undefined 表示未初始化的变量;null 表示空对象指针;null == undefined

Boolean 类型:其他类型转换为 Boolean 类型,使用函数 Boolean()。

Number 类型:表示整数和浮点数值。

NaN 即非数值(Not a Number)是一个特殊的数值,表示一个原本要返回数值的操作未返回数值的情况。

任何涉及 NaN 的操作都会返回 NaN;NaN与任何值都不等,包括 NaN 本身。
NaN 类型可以使用 isNaN() 函数检测。

数值转换:有3个函数可以把非数值转换为数值:Number()、parseInt()、parseFloat()。
Number()可以用于任何数据类型,而另两个函数则专门用于把字符串转换成数值。

Number() 函数转换:null和空字符串返回 0;undefined和无法转换的字符串返回 NaN ;对象依次尝试调用 valueOf()、toString()方法,根据函数返回值进行转换。
parseInt() 函数转换:忽略字符前的空格,找到第一个非空格字符;如果第一个字符不是数字字符或者负号,返回 NaN ;继续解析遇到非数字字符为止。可选第二个参数指定进制。
parseFloat() 函数转换:和 parseInt() 函数类似,但是字符串中的第一个小数点是有效的。

String 类型:符串是不可变的,要改变要先删除。
其他类型转换为 String 类型,使用函数 toString() 或 String() 或加一个空字符串("")。

数值、布尔值、对象、字符串值 都有 toString() 方法。null、undefined 值没有。
String() :null 返回 "null" , undefined 返回 "undefined"。其他调用 toString() 方法。

Object 类型:一组数据(属性)和功能(方法)的组合。

创建对象的方法:

var o = new Object();

在 ECMAScript 中,Object 类型是其他所有实例的基础,Object 类型具有的属性和方法也同样存在于更具体的对象中。
Object 的每个实例(对象)都具有下列属性和方法:

  • constructor:保留着用于创建当前对象的函数即构造函数;
  • hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中是否存在;
  • isPrototypeOf(object):用于检查传入的对象是否是传入对象的原型;
  • propertyIsEnumerable();toLocaleString();
  • toString():返回对象的字符串表示;
  • valueOf():返回对象的字符串、数值或布尔值表示。

操作符

操作符包括:算数操作符、位操作符、关系操作符、相等操作符。

用法和概念基本和 Java 一致,相同部分不再记笔记。

在比较字符串时,实际比较的是两个字符串中对应位置的每个字符的字符编码值。

"23"<"3" //true

相等操作符基本规则:
null == undefined,且不转换;
对象除非指向同一个否则互相不等;
操作值是数值,则另一个值转换为数值进行比较;
操作值是布尔值,则将布尔值转换为数值再进行比较,false转换为0,true转换为1。

true == 1 ; //true
true == 2 ; //false
NaN == NaN ; //false
NaN != NaN ; //true

全等和不全等:两个操作数未经转换就相等为全等;转换之后相等为非全等

"23" == 23 ; //true
"23" === 23 ; //false

语句

for-in 语句可以用来枚举对象的属性。

for (property in expression) {
  ...
}

break 和 continue 语句与 label 语句联合使用:多发生在循环嵌套的情况下。

var num = 0;
outermost:
for (var i = 0; i < 10; i++) {
  for (var j = 0; j < 10; j++) {
    if (i == 5 && j ==5) {
      break outermost;
    }
    num++;
  }
}
console.log(num);  // 55

函数

函数参数:参数在内部是用一个数组来表示的,函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。
在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数。

没有重载:ECMAScript 函数不能像传统意义上那样实现重载。如果在ECMAScript 中定义了两个名字相同的函数,则该名字只属于后定义的函数。

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

JavaScript 变量的特征:本质是松散类型,决定了它只是在特定时间用于保存特定值的一个名字而已。由于不存在定义某个变量必须要保存何种数据类型值的规则,变量的值及其数据类型可以在脚本的生命周期内改变。

基本类型和引用类型的值

基本类型指的是简单的数据段,而引用数据类型指那些可能由多个值构成的对象。
Undefined、Null、Boolean、Number 和 String 这 5 种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。
引用类型的值是保存在内存中的对象。在操作对象时,实际上是在操作对象的引用而不是实际的对象。

动态的属性:对引用类型可添加属性和方法,也可改变和删除其属性和方法;对基本类型也能添加属性,但基本类型并不会保存,操作是无效的。

复制变量值:从一个变量向另一个变量复制值,基本类型会创建新值并复制到新变量;而引用类型只会复制指向对象的指针,新旧变量会相互影响。

传递参数:函数的参数是按值传递时的,即把函数外部的值复制给函数内部的参数(参数实际上是函数的局部变量),就和把值从一个变量复制到另一个变量一样。
基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样,复制的是一个指向对象的指针。

function setName(obj){
  obj.name = 'Nicholas';
  obj = new Object();
  obj.name = 'Greg';
}
var person = new Object();
setName(person);
console.log(person.name);  // => Nicholas

对于引用类型,当把参数名指向新的内存空间,再对其做添加属性等操作,不会影响到原来传递的那个对象,所以不是按引用传递参数。

检测类型:instanceof 根据原型链,检测变量是什么引用类型的实例。

检测基本数据类型 typeof 非常有用,但如果变量是对象或 null ,typeof 只能返回 “Object” 。

执行环境和作用域

执行环境:定义了变量或函数有权访问的其他数据,决定了它们各自的行为。有全局执行环境和局部(函数)执行环境之分。

作用域链:搜索变量和函数的作用域链,保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端是当前执行的代码所在的变量环境,最后一个对象是全局执行环境的变量对象。

延长作用域链:try-catch 语句的 catch 块和 with 语句。

没有块级作用域:由花括号封闭的代码没有自己的作用域。如 if 、for 语句中只是在语句的执行环境中(全局或函数)。

查询标识符:从作用域链的前端开始,向上逐级查询,找到后搜索结果停止,没有找到则一直追溯到全局环境的变量对象。

垃圾收集

JavaScript 具有自动垃圾回收机制,即执行环境会负责管理代码执行过程中使用的内存。

最常用的方法有标记清除和引用计数。

管理内存:最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。解除一个值的引用并不意味着自动回收该值所占用的内存——解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

第五章 引用类型

引用类型的值(对象)是引用类型(类、对象定义)的一个实例。

Object 类型

创建对象:

// 1、使用 Object 构造函数
var person = new Object();
person.name = "Nicholas";
person.age = 29;
// 2、对象字面量表示法
var person = {
    name : "Nicholas",
    age : 29
};
// 属性名可以使用字符串
var person = {
    "name" : "Nicholas",
    "age" : 29,
    5 : true //自动转换为字符串
};
// 与 new Object() 等价
var person = {};
person.name = "Nicholas";
person.age = 29;

访问对象属性:

// 1、点表示法
person.name
// 2、方括号表示法
person["name"]

// 通过变量访问
var n = 'name';
console.log(person[n]);
// 包含语法错误的字符
onsole.log(person['first name'];

Array 类型

ECMAScript 数组的每项可保存任何类型的数据,没项类型可不同;大小可自动调整。

创建数组:

var a1 = new Array();
var a2 = new Array(20);
var a3 = new Array('red', 'blue', 'green');

var a4 = [];
var a5 = ['red', 'blue', 'green'];

检测数组:Array.isArray() 方法可检测对象是不是数组。

转换方法:toLocaleString()、toString()和 valueOf()方法。

toLocaleString()、toString() 方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。
toLocaleString() 方法为了取得每一项的值,调用的是每一项的 toLocaleString() 方法。
valueOf() 方法返回的还是数组。

var person1 = {
  toLocaleString : function () {
      return "Nikolaos";
  },
  toString : function() {
      return "Nicholas";
  }
};

var person2 = {
  toLocaleString : function () {
      return "Grigorios";
  },
  toString : function() {
      return "Greg";
  }
};

var people = [person1, person2];
console.log(people);                      //Nicholas,Greg
console.log(people.toString());           //Nicholas,Greg
console.log(people.toLocaleString());     //Nikolaos,Grigorios

join() 方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。

var colors = ["red", "green", "blue"];
console.log(colors.join(","));      //red,green,blue
console.log(colors.join("||"));     //red||green||blue

栈方法:push() 和 pop() 方法。

push() 方法接收任意数量的参数,把他们逐个加到数组末尾,并返回修改后的数组长度。
pop() 方法从数组末尾移除最后一项,减少数组长度,然后返回移除项。

队列方法:shift() 和 unshift() 方法。

shift() 从数据前端取项方法。类比 pop() 。
unshift() 从数组前端添加项。类比 push() 。

重排序方法:reverse() 和 sort() 方法。

reverse() 反转数组项的顺序。
sort() 默认升序排列数组项,调用每项的 toString() 转型方法,然后比较字符串。

sort() 方法可以接受一个比较函数作为参数。比较函数接受两个参数,第一个参数在第二个之前返回负数。

// 从小到大:
function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
var values = [0, 1, 5, 10, 15];
values.sort(compare);
console.log(values);    //0,1,5,10,15

// 或者
function compare(value1, value2) {
    return value1 < value2;
}

var values = [0, 1, 5, 10, 15];
values.sort(compare);
console.log(values);    //0,1,5,10,15

操作方法 :concat() slice() splice() 。

concat() 添加项。创建新数组。

var colors = ["red", "green", "blue"];
var colors2 = colors.concat("yellow", ["black", "brown"]);

console.log(colors);     //red,green,blue
console.log(colors2);    //red,green,blue,yellow,black,brown

slice() 截取。创建新数组。

在只有一个参数的情况下,slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。
如果有两个参数,该方法返回起始和结束位置之间的项——但不包括结束位置的项。

var colors = ["red", "green", "blue", "yellow", "purple"];
var colors2 = colors.slice(1);
var colors3 = colors.slice(1,4);

console.log(colors2);   //green,blue,yellow,purple
console.log(colors3);   //green,blue,yellow

splice() 删除插入替换。改变原数组。

三个参数:起始位置、要删除的项数、要插入的项。
第三个不传是删除功能,第二个为 0 是插入功能,都有则替换。
返回:删除的项。

var colors = ["red", "green", "blue"];
var removed = colors.splice(0,1);              //remove the first item
console.log(colors);     //green,blue
console.log(removed);    //red - one item array

removed = colors.splice(1, 0, "yellow", "orange");  //insert two items at position 1
console.log(colors);     //green,yellow,orange,blue
console.log(removed);    //empty array

removed = colors.splice(1, 1, "red", "purple");    //insert two values, remove one
console.log(colors);     //green,red,purple,orange,blue
console.log(removed);    //yellow - one item array

位置方法:indexOf() lastIndexOf() 接收两个参数:要查找的项和(可选)查找起点位置的索引;indexOf()从前往后查找,lastIndexOf()从后往前查找;返回要查找的项的位置,没找到则返回 -1。

迭代方法:两个参数:在每项运行的函数和(可选的)作用域对象;其中函数接收三个参数:数组项的值、该项在数组中的位置、数组对象本身。

every():对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true。有返回false的项时就不会再对后面的项检测了,直接返回false。
some():对数组中的每一项运行给定函数,如果该函数对任一项返回true,则返回true。有返回true的项时就不再对后面的项检测了,直接返回ture。

filter():对数组中的每一项运行给定函数,返回该函数会返回true的项组成的数组。

map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
forEach():对数组中的每一项运行给定函数。这个方法没有返回值。本质上与使用 for 循环迭代数组一样。

var a = [1, 2, 3, 4, 5, 4, 3, 2, 1];

var everyResult = a.every(function (item, index, array) {
  return (item > 2);
});
console.log(everyResult);  // false

var someResult = a.some(function (item, index, array) {
  return (item > 2);
});
console.log(someResult);  // true

var filterResult = a.filter(function (item, index, array) {
  return (item > 2);
});
console.log(filterResult);  // [3, 4, 5, 4, 3]

var mapResult = a.map(function (item, index, array) {
  return (item * 2);
});
console.log(mapResult);  // [2, 4, 6, 8, 10, 8, 6, 4, 2]

var forEachResult = a.forEach(function (item, index, array) {
  console.log(item);
});
console.log(forEachResult);  // undefined

缩小方法(递归方法):reduce() 和 reduceRight() 迭代数组的所有项,然后构建一个最终返回的值;reduce()方法从前往后,reduceRight()从后往前。
reduce() 和 reduceRight() 接收两个参数:一个在每项上调用的函数和(可选的)作为缩小基础的初始值。其中函数接收4个参数:前一个值、当前值、项的索引和数组对象。函数的返回值又会作为第一个参数自动传给下一项。

var values = [1,2,3,4,5];
var sum = values.reduce(function(prev, cur, index, array){
  return prev + cur;
},1);
console.log(sum); //16

Date 类型

创建日期对象:

var now = new Date();
var date = new Date(2005, 4, 5, 17, 55, 55);  // 2005年5月5日下午5点55分55秒

获取调用时的日期和时间和毫秒数:

var start = Date.now();
doSomething();
var stop = Date.now();
var result = stop - start;

日期的格式化方法:

var date = new Date(2015, 2, 5, 17, 55, 55);
date.toString();  // "Thu Mar 05 2015 17:55:55 GMT+0800 (CST)"
date.toDateString();  // "Thu Mar 05 2015"
date.toTimeString();  // "17:55:55 GMT+0800 (CST)"
date.toLocaleString();  // "2015/3/5 下午5:55:55"
date.toLocaleDateString();  // "2015/3/5"
date.toLocaleTimeString();  // "下午5:55:55"

RegExp 类型

创建正则表达式:

var exp1 = / pattern / flags ;
var exp2 = new RegExp('pattern', 'flags');

模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。
标志(flags)用以标明正则表达式的行为。正则表达式的匹配模式支持下列3 个标志:

  • g:表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止;
  • i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
  • m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。

RegExp 实例方法:

exec():返回第一个匹配项信息的数组,数组第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串;还包含两个额外的属性,index 和 input。
test():在模式与该参数匹配的情况下返回true,否则返回false。

正则表达式:http://www.runoob.com/regexp/regexp-tutorial.html

Function 类型

函数实际上是对象。函数名实际上是指向函数对象的指针。
每个函数都是 Function 类型的实例,而且与其他引用类型一样具有属性和方法。

// 函数声明
function sum (num1, num2){
  return num1 + num2;
}
// 函数表达式
var sum = function(num1, num2){
  return num1 + num2;
};
// 使用构造函数,不推荐
var sum = new Function('num1', 'num2', 'return num1 + num2');

函数声明与函数表达式:解释器会率先读取函数声明,并使其在执行任何代码之前可用(函数声明提升);函数表达式必须等到解释器执行到它所在行才会真正被解释执行。

// 函数声明
console.log(sum(10,10));
function sum (num1, num2){
  return num1 + num2;
}
// 函数表达式
console.log(sum(10,10));// Uncaught TypeError: sum is not a function
var sum = function(num1, num2){
  return num1 + num2;
};

作为值的函数:因为 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。

函数的内部属性:arguments 、this 。

arguments: 是一个类数组对象,包含着传入函数中的所有参数。

function factorial(num){
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num-1)
    }
}

function factorial(num){
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num-1)
    }
}

arguments 的属性 callee 指向拥有此 arguments 的函数。

// 无论引用函数时使用的什么名字,都可以保证正常完成递归。
var trueFactorial = factorial;

factorial = function(){
    return 0;
};
console.log(trueFactorial(5));   //120
console.log(factorial(5));       //0

this 引用的是函数据以执行的环境对象。

在全局作用域中调用函数,this 引用的是全局对象 window;
把函数赋给对象 o 并调用时,this 引用的就是对象 o 。

caller : 调用当前函数的函数的引用,返回后者的源代码。

函数的属性和方法

函数的两个属性:length 和 prototype 。

length:表示函数希望接收的命名参数的个数。

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

函数的两个非继承而来的方法:apply() 和 call() 。在特定的作用域中调用函数。

apply():接受两个参数,运行函数的作用于和参数数组。

function sum(num1, num2){
    return num1 + num2;
}
// 参数数组可以是 Array 的实例,也可以是 arguments 对象。
function callSum1(num1, num2){
    return sum.apply(this, arguments);
}
function callSum2(num1, num2){
    return sum.apply(this, [num1, num2]);
}
console.log(callSum1(10,10));   //20
console.log(callSum2(10,10));   //20

call():必须明确地传入每个参数。

function sum(num1, num2){
    return num1 + num2;
}
function callSum(num1, num2){
    return sum.call(this, num1, num2);
}
console.log(callSum(10,10));   //20

apply() 和 call() 真正强大的地方是能够扩充函数赖以运行的作用域。而使用它们来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。

window.color = "red";
var o = { color: "blue" };
function sayColor(){
    console.log(this.color);
}
sayColor();            //red
sayColor.call(this);   //red
sayColor.call(window); //red
sayColor.call(o);      //blue

bind() 方法会创建一个函数的实例,其 this 值会绑定到传给 bind() 函数的值。

var objectSayColor = sayColor.bind(o);
objectSayColor();   //blue

基本包装类型

Boolean 类型、Number 类型、String 类型。

单体内置对象

Global 对象、Math 对象。

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

对象:无序属性的集合,其属性可以包含基本值、对象或者函数。

理解对象

属性在创建时都带了一些特征值(characteristic),JavaScript 通过这些特征值来定义他们的行为。

ECMAScript 中有两种属性:数据属性和访问器属性。
描述属性的各种特征,是为了实现JavaScript引擎用的,不能直接访问。

数据属性

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改为访问器属性。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。
  • [[Writeable]]:表示能否修改属性的值。
  • [[Value]]:包含这个属性的数据值。

访问器属性

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改为数据属性。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。
  • [[Get]]:在读取属性时调用的函数。
  • [[Set]]:在写入属性时调用的函数。

定义及读取特性:Object.defineProperty() Object.defineProperties();Object.getOwnPropertyDescriptor() 。

创建对象

虽然使用 Object 构造函数或者对象字面量可以创建单个对象,但使用同一接口创建很多对象,会产生大量的重复代码。

工厂模式:使用函数来封装以特定接口来创建对象。

function createPerson(name,age,job){
  var o=new Object();
  o.name=name;
  o.age=age;
  o.job=job;
  o.sayName=function(){
    console.log(this.name);
  };
  return o;
}
var person1=createPerson("Greg",27,"Doctor");

构造函数模式

function Person(name,age,job){
  this.name=name;
  this.age=age;
  this.job=job;
  this.sayName=function(){
    console.log(this.name);
  };
}
var person1=new Person("Greg",27,"Doctor");

要创建Person的实例,必须使用new操作符。用这种方式调用构造函数需要4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

构造函数和其他函数唯一的区别,就是在于调用他们的方式不同。
任何函数,只要通过 new 操作符来调用,那么它就可以作为构造函数。

原型模式

创建的每一个函数都有一个 prototype(原型)属性,指向一个对象;这个对象(原型对象 )的用途是包含可以由特定类型的所有实例共享的属性和方法。
prototype 就是通过调用构造函数而创建的实例的原型对象。好处是可以让所有的实例共享原型对象包含的属性和方法,不用在构造函数里面定义实例的信息,而是直接添加到原型对象中。

function Person(){
}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName();   //"Nicholas"

var person2 = new Person();
person2.sayName();   //"Nicholas"

console.log(person1.sayName == person2.sayName);  //true

1.理解原型对象:

只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,指向原型对象;
默认所有原型对象都会获得一个 constructor 属性,指向 prototype 属性所在函数(构造函数);
调用构造函数创建实例后,实例内部将包含一个指针 [[Prototype]] 或者 __proto__ 指向构造函数的原型对象。

向对象的程序设计-理解原型对象

···
// 使用 isPrototype() 来检测构造函数的原型对象和实例对象之间是否有关系
console.log(Person.prototype.isPrototypeOf(person1));  //true
console.log(Person.prototype.isPrototypeOf(person2));  //true
// Object.getPrototypeOf() 返回实例对象的原型
if (Object.getPrototypeOf){
    console.log(Object.getPrototypeOf(person1) == Person.prototype);  //true
    console.log(Object.getPrototypeOf(person1).name);  //"Nicholas"
}

当代码读取某个对象的属性时,先从对象实例本身开始,若没有找到才会在原型对象中查找。

当为对象实例添加属性时,这个属性会屏蔽掉原型对象中的同名属性,即使将这个属性设置为 null ,也只会在实例中设置这个属性。不过使用 delete 操作符可以完全删除实例属性,使得可以重新访问原型中的属性。

···
person1.name = "Greg";
console.log(person1.name);   //"Greg" - 来自实例
console.log(person2.name);   //"Nicholas" - 来自原型

delete person1.name;
console.log(person1.name);   //"Nicholas" - 来自原型

// 使用 hasOwnProperty() 来检测属性存在于实例中还是原型中
console.log(person1.hasOwnProperty("name")); // false - 来自原型
person1.name = "Greg";
console.log(person1.hasOwnProperty("name")); // true - 来自实例
delete person1.name;
console.log(person1.hasOwnProperty("name")); // false - 来自原型

实例与原型的关系:
向对象的程序设计-理解原型对象2

2.原型与 in 操作符:单独使用和在 for-in 循环中使用。

···
// 单独使用时无论属性在原型还是在实例中都返回 true
console.log("name" in person1); // true
person1.name = "Greg";
console.log("name" in person1);   //true

// for-in 循环时,返回的是所有能通过对象访问的、可枚举的属性;同样也包括原型和实例中的。
for (var prop in person1) {
  console.log(prop);  // name age job sayName
}

Object.keys() 方法可以取得对象上所有可枚举的实例属性。

···
var keys = Object.keys(Person.prototype);
console.log(keys);   //"name,age,job,sayName"

Object.getOwnPropertyNames() 方法可以得到所有实例属性,无论其是否可枚举。

···
var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);   //"constructor,name,age,job,sayName"

3.更简单的原型语法:

function Person(){
}
// 本质是重写了默认的 prototype 对象
// constructor 属性指向 Object
Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};
console.log(friend instanceof Object);  //true
console.log(friend instanceof Person);  //true
console.log(friend.constructor == Person);  //false
console.log(friend.constructor == Object);  //true

为解决 constructor 无法确定对象类型的问题:

function Person(){
}
// 方法1:
Person.prototype = {
    // 使 constructor 属性指向构造函数
    constructor : Person,
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};

// 方法2:
Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};
// 设置 constructor 属性不可枚举
Object.defineProperty(Person.prototype."constructor"){
  enumerable : false,
  value: Person
}

// 结果相同
var friend = new Person();
console.log(friend instanceof Object);  //true
console.log(friend instanceof Person);  //true
console.log(friend.constructor == Person);  //true
console.log(friend.constructor == Object);  //false

4.原型的动态性:

我们对原型的修改都能立刻反映到实例上。
但是重写原型,实例对象还是指向原来的原型,不会指向新重写的原型对象。
重写原型切断了新的原型和之前已经存在的对象实例间的联系。

5.原型对象的问题:原型对象中讯在引用类型的属性,会使所有实例共享该引用。

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

构造函数用于定义实例属性,原型模式用于定义方法和共享的属性。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    constructor: Person,
    sayName : function () {
        console.log(this.name);
    }
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");

console.log(person1.friends);    //"Shelby,Court,Van"
console.log(person2.friends);    //"Shelby,Court"
console.log(person1.friends === person2.friends);  //false
console.log(person1.sayName === person2.sayName);  //true

实例属性都是在构造函数中定义的,每个实例都会有自己的一份实例属性的副本。

动态原型模式:将原型信息封装在构造函数中。

function Person(name, age, job){
    //properties
    this.name = name;
    this.age = age;
    this.job = job;
    //methods
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        };
    }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

寄生构造函数模式稳妥构造函数模式

继承

ECMAScript 只支持实现继承,主要依靠原型链来实现。

原型链:利用原型将一个引用类型继承另一个引用类型的属性和方法。

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}
//继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue());   //true

实例以及构造函数和原型之间的关系:

向对象的程序设计-原型链

getSuperValue() 方法仍然在 SuperType.prototype 中,但 property 位于 SubType.prototype 中。
因为我们重写了 SubType 的原型,新原型即 SuperType 的实例;property 是实例的属性,getSuperValue() 则是 SuperType 原型的方法。

1.注意默认原型:函数的默认原型是 Object 实例,因此默认原型会包含指针指向 Object.prototype 。

2.确定实例与原型的关系:

console.log(instance instanceof Object);      //true
console.log(instance instanceof SuperType);   //true
console.log(instance instanceof SubType);     //true
console.log(Object.prototype.isPrototypeOf(instance));    //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance));   //true

3.谨慎地定义方法:给原型添加方法一定要在替换原型之后;也不能使用字面量创建原型方法。

4.原型链问题:引用类型的原型属性会被所有实例共享;不能向超类传递参数。

借用构造函数:通过 apply() 或 call() 方法在新创建的对象上执行超类的构造函数。

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    //继承自 SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
console.log(instance2.colors);    //"red,blue,green"

通过 apply() 或 call() 方法在 SubType 实例的环境下调用了 SuperType 构造函数。

1.传递参数:

function SuperType(name){
    this.name = name;
}
function SubType(){
    //继承自 SuperType,传递参数
    SuperType.call(this, "Nicholas");
    //实例属性
    this.age = 29;
}
var instance = new SubType();
console.log(instance.name);    //"Nicholas";
console.log(instance.age);     //29

2.问题: 和构造函数模式类似,方法都在构造函数中定义,无法复用。

组合继承:JavaScript中最常用的继承。融合了原型链和构造函数的优点。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    console.log(this.name);
};

function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}

SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);  //"red,blue,green,black"
instance1.sayName();      //"Nicholas";
instance1.sayAge();       //29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors);  //"red,blue,green"
instance2.sayName();      //"Greg";
instance2.sayAge();       //27

原型式继承寄生式继承寄生组合式继承

第七章 函数表达式

定义函数的方法有两种:函数声明与函数表达式

递归

递归函数是在一个函数通过名字调用自身函数构成。

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * factorial(num-1);
    }
}

var anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(4));  //error! - factorial 已经不是函数

解决办法是使用命名函数表达式:

function factorial = (function f(num){
    if (num <= 1){
        return 1;
    } else {
        return num * f(num-1);
    }
});

闭包

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

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

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

其中每个函数都引用着保存变量 i 的同一个变量对象,所以函数内部返回的值都是 10 。

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

2.关于 this 对象:匿名函数通常具有全局性,因此其 this 对象通常指向 windows 。

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
console.log(object.getNameFunc()());  //"The Window" 在非严格模式下

this 的值有时候会意外发生改变。

var name = "The Window";
var object = {
    name : "My Object",
    getName: function(){
        return this.name;
    }
};
console.log(object.getName());     //"My Object"
console.log((object.getName)());   //"My Object"
console.log((object.getName = object.getName)());   //"The Window" 在严格模式下
}

第三个其实是重写了 getName 方法,this 值就指向全局了。

模仿块级作用域

JavaScript 没有块级作用域(私有作用域)的概念,可使用匿名函数方法模仿。

(function () {
    ...
})();

私有变量

JavaScript 没有私有成员的概念,所有对象属性都是公有的,但是有个私有变量的概念,也就是函数中定义的变量。

function Person(name){
    this.getName = function(){
        return name;
    };
    this.setName = function (value) {
        name = value;
    };
}
var person = new Person("Nicholas");
console.log(person.getName());   //"Nicholas"
person.setName("Greg");
console.log(person.getName());   //"Greg"

创建 Person 实例只能 通过 getName() 和 setName() 方法访问内部变量,但是针对每个实例都会创建一组新方法。

1.静态私有变量

(function(){
    var name = "";
    Person = function(value){
        name = value;
    };
    Person.prototype.getName = function(){
        return name;
    };
    Person.prototype.setName = function (value){
        name = value;
    };
})();

var person1 = new Person("Nicholas");
console.log(person1.getName());   //"Nicholas"
person1.setName("Greg");
console.log(person1.getName());   //"Greg"

var person2 = new Person("Michael");
console.log(person1.getName());   //"Michael"
console.log(person2.getName());   //"Michael"

初始化未声明的变量,总会创建一个全局变量。严格模式下报错。

示例代码中 name 变成了一个静态的,所有实例共享的属性。

2.模块模式

模块模式是为单例创建私有变量和特权方法。单例即只有一个实例的对象。

function BaseComponent(){
}
function OtherComponent(){
}
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);
            }
        }
    };
}();

application.registerComponent(new OtherComponent());
console.log(application.getComponentCount());  //2

3.增强的模块模式

针对单例必须是某种类型的实例,同时还对其添加属性或方法的情况。

function BaseComponent(){
}
function OtherComponent(){
}
var application = function(){
    //私有变量和方法
    var components = new Array();
    //初始化
    components.push(new BaseComponent());
    //创建 application 的一个局部副本
    var app = new BaseComponent();
    //公共接口
    app.getComponentCount = function(){
        return components.length;
    };
    app.registerComponent = function(component){
        if (typeof component == "object"){
            components.push(component);
        }
    };
    //返回副本
    return app;
}();

console.log(application instanceof BaseComponent);
application.registerComponent(new OtherComponent());
console.log(application.getComponentCount());  //2

如果本文对您有所帮助,且您手头还很宽裕,欢迎打赏赞助我,以支付网站服务器和域名费用。 https://paypal.me/wshunli 您的鼓励与支持是我更新的最大动力,我会铭记于心,倾于博客。
本文链接:https://www.wshunli.com/posts/e8afa8c0.html