最近抽空拜读了下尼古拉斯的《javascript面向对象精要》,这本书算是高程3中的节选章节,不过作者换了一种思路,讲的非常精简明了,巩固,巩固,巩固.....
JS面向对象精要(一)_原始类型和引用类型
JS面向对象精要(二)_函数
JS面向对象精要(三)_理解对象
JS面向对象精要(四)_构造函数和原型对象
JS面向对象精要(五)_继承
面向对象语言
面向对象的语言有如下几种特性。
- 封装 数据可以和操作数据的功能组织在一起。这就是对象的定义,十分简单。
- 聚合 一个对象能够引用另一个对象。
- 继承 一个新创建的对象和另一个对象拥有同样的特性,而无需显式复制其功能。
- 多态 一个接口可被多个对象实现。
原始类型和引用类型
几乎所有 JavaScript 的数据要么是一个对象要么从对象中获取。其实就连函数在 JavaScript 中也被视为对象,这使得它们成为 JavaScript 的一等公民。
1.1 什么是类型
JavaScript 虽然没有类的概念,但依然存在两种主要类型:原始类型和引用类型。原始类型保存为简单数据值。引用类型则保存为对象,其本质是指向内存位置的引用。为了让开发者能够把原始类型和引用类型按相同方式处理,JavaScript 花费了很大努力来保证语言的一致性。
1.2 原始类型
原始类型代表照原样保存的一些简单数据,如 true 和 25。JavaScript 共有 5 种原始类型:
- Boolean 布尔,值为 true 或 false
- Number 数字,值为任何整型或浮点数值
- String 字符串,值为由单引号或双引号括出的单个字符或连续字符(JavaScript 不区分字符类型)
- Null 空类型,该原始类型仅有一个值:null
- Undefined 未定义,该原始类型仅有一个值:undefined(undefined 会被赋给一个还没有初始化的变量)
原始类型的变量直接保存原始值(而不是一个指向对象的指针)。当你将原始值赋给一个变量时,该值将被复制到变量中。也就是说,如果你使一个变量等于另一个时,每个变量有它自己的一份数据拷贝
/*
虽然color1和color2具有同样的值,但是两者毫无关联,
改变color1的值不会影响color2,反之亦然。这是因为存在两个不同的储存地址,每个变量拥有一个
*/
var color1 = "red";
var color2 = color1;
1.2.1 鉴别原始类型
鉴别原始类型的最佳方法是使用 typeof 操作符。它可以被用在任何变量上,并返回一个说明数据类型的字符串。Typeof 操作符可用于字符串、数字、布尔和未定义类型,在逻辑上,你可以认为 null 是一个空的对象指针,所以结果为“object”
//判断一个值是否为空类型的最佳方法是直接和null比较
console.log(value === null);
当你使用双等号进行比较时,双等号操作符会在比较之前把字符串转换成数字,因此认为字符串“5”和数字 5 相等。
三等号操作符认为这两个值的类型不同,因此不相等。同样原因,当你比较 undefined 和 null 时,双等号认为它们相等而三等号认为不相等。当你试图鉴别 null 时,使用三等号才能让你正确鉴别出类型。
console.log("5" == 5); // true
console.log("5" === 5); // false
console.log(undefined == null); // true
console.log(undefined === null); // false
1.2.2 原始方法
虽然字符串、数字和布尔是原始类型,但是它们也拥有方法(null 和 undefined 没有方法)。特别是字符串有很多方法。尽管原始类型拥有方法,但它们不是对象。JavaScript 使它们看上去像对象一样,以此来提供语言上的一致性体验
var name = "Nicholas";
var lowercaseName = name.toLowerCase(); // convert to lowercase
var firstLetter = name.charAt(0); // get first character
var middleOfName = name.substring(2, 5); // get characters 2-4
1.3 引用类型
引用类型是指 JavaScript 中的对象,对象是属性的无序列表。属性包含键(始终是字符串)和值。如果一个属性的值是函数,它就被称为方法。JavaScript 中函数其实是引用值,除了函数可以运行以外,一个包含数组的属性和一个包含函数的属性没有什么区别
1.3.1 创建对象
使用 new 操作符和构造函数。构造函数就是通过 new 操作符来创建对象的函数——任何函数都可以是构造函数。根据命名规范,JavaScript 中的构造函数用首字母大写来跟非构造函数进行区分
// 实例化一个通用对象,并将它的引用保存在object中
var object = new Object();
因为引用类型不在变量中直接保存对象,所以本例中的 object 变量实际上并不包含对象的实例,而是一个指向内存中实际对象所在位置的指针(或者说引用)。这是对象和原始值之间的一个基本差别,原始值是直接保存在变量中的
1.3.2 对象引用解除
JavaScript 语言有垃圾收集的功能,因此当你使用引用类型时无需担心内存分配。但最好在不使用对象时将其引用解除,让垃圾收集器对那块内存进行释放。解除引用的最佳手段是将对象变量置为 null。
当内存中的对象不再被引用后,垃圾收集器会把那块内存挪作它用(在那些使用几百万对象的巨型程序里,对象引用解除尤其重要)
var object1 = new Object();
object1 = null; // dereference
1.3.3 添加删除属性
var object1 = new Object();
var object2 = object1;
object1.myCustomProperty = "Awesome!";
console.log(object2.myCustomProperty); // "Awesome!"
1.4 内建类型实例化
Object 类型只是 JavaScript 提供的少量内建引用类型之一。其他内建类型各有它们的特殊用途,可在任何时候被实例化
/*
Array 数组类型,以数字为索引的一组值的有序列表
Date 日期和时间类型
Error 运行期错误类型(还有一些更特别的错误的子类型)
Function 函数类型
Object 通用对象类型
RegExp 正则表达式类型
*/
var items = new Array();
var now = new Date();
var error = new Error("Something bad happened.");
var func = new Function("console.log('Hi');");
var object = new Object();
var re = new RegExp("\\d+");
1.4.1 字面形式
内建引用类型有字面形式。字面形式允许你在不需要使用 new 操作符和构造函数显式创建对象的情况下生成引用值(你曾在前面见过原始类型的字面形式,包括字符串、数字、布尔、空类型和未定义)
1.4.2 对象和数字的字面形式
虽然使用字面形式并没有调用 new Object(),但是 JavaScript 引擎背后做的工作和 new Object()一样,除了没有调用构造函数。其他引用类型的字面形式也是如此
var book = {
name:"The Principles of Object-Oriented JavaScript",
year:2014
};
// 属性名字也可以用字符串表示,特别是当你希望名字中包含空格或其他特殊字符时。
var book = {
"name":"The Principles of Object-Oriented JavaScript",
"year":2014
};
// 本例等价于前例,仅在语法上有所区别。下例是另一种等价写法。
var book = new Object();
book.name = "The Principles of Object-Oriented JavaScript";
book.year = 2014;
var colors = [ "red", "blue", "green" ];
console.log(colors[0]); // "red"
// 这段代码等价于:
var colors = new Array("red", "blue", "green")
console.log(colors[0]); // "red"
1.4.3 函数字面形式
考虑到在可维护性、易读性和调试上的巨大挑战,通常不会有人使用函数的构造函数,即使是这样一个简单的例子,使用字面形式都比构造函数的形式方便和易读。另外,用构造函数创建的函数没什么好的调试方法:JavaScript 调试器认不出这些函数,它们在程序里就好像黑盒一
function reflect(value) {
return value;
}
// 等价于
var reflect = new Function("value", "return value;");
1.4.4 正则表达式字面量
使用字面形式比较方便的一个原因是你不需要担心字符串中的转义字符。如果使用 RegExp 构造函数,传入模式的参数是一个字符串,你需要对任何反斜杠进行转义(这就是为什么字面形式使用“\d”而构造函数使用“\”d 的原因)。在 JavaScript 中,除非需要通过一个或多个字符串动态构造正则表达式,否则都建议使用字面形式而不是构造函数。
//看上去类似Perl中的正则表达式:模式被包含在两个“/”之间,第二个“/”后是由单字符表示的额外选项
var numbers = /\d+/g;
// is the same as
var numbers = new RegExp("\\d+", "g");
1.5 访问属性
var array = [];
array.push(12345);
// 也可以如下例用中括号,方法的名字现在由中括号中的字符串表示。
var array = [];
array["push"](12345);
在需要动态决定访问哪个属性时,这个语法特别有用。例如下例的中括号允许你用变量而不是字符串字面形式来指定访问的属性。
var array = [];
var method = "push";
array[method](12345);
在这段代码中,变量 method 的值是“push”,因此在 array 上调用了 push()方法。这种能力极其有用,除了语法不同,在性能或其他方面点号和中括号都大致相同,唯一区别在于中括号允许你在属性名字上使用特殊字符。开发者通常认为点号更易读,所以你更多地看到点号而不是中括号
1.6 鉴别引用类型
函数是最容易鉴别的引用类型,因为对函数使用 typeof 操作符时,返回值是“function”
对于所有非函数的引用类型,typeof 返回“object”。在处理很多不同类型的时候这帮不上什么忙。为了更方便地鉴别引用类型,可以使用 JavaScript 的 instanceof 操作符
instanceof 操作符以一个对象和一个构造函数为参数。如果对象是构造函数所指定的类型的一个实例,instanceof 返回 true;否则返回 false
var items = [];
var object = {};
function reflect(value) {
return value;
}
console.log(items instanceof Array); // true
console.log(object instanceof Object); // true
console.log(reflect instanceof Function); // true
/*
instanceof操作符可鉴别继承类型。这意味着所有对象都是Object的实例,
因为所有引用类型都继承自Object。
*/
console.log(items instanceof Object); // true
console.log(object instanceof Object); // true
console.log(reflect instanceof Object); // true
1.7 鉴别数组
虽然 instanceof 可以鉴别数组,但是有一个例外会影响网页开发者:JavaScript 的值可以在同一个网页的不同框架之间传来传去。当你试图鉴别一个引用值的类型时,这就有可能成为一个问题,因为每一个页面拥有它自己的全局上下文——Object、Array 以及其他内建类型的版本。结果,当你把一个数组从一个框架传到另一个框架时,instanceof 就无法识别它,因为那个数组是来自不同框架的 Array 的实例。
为了解决这个问题,ECMAScript 5 引入了 Array.isArray()来明确鉴别一个值是否为 Array 的实例,无论该值来自哪里,该方法对来自任何上下文的数组都返回 true。如果你的环境兼容 ECMAScript 5,Array.isArray()是鉴别数组的最佳方法。
// 大多数环境都在浏览器和Node.js中支持Array.isArray()方法。IE8或更早的版本不支持该方法。
var items = [];
console.log(Array.isArray(items)); // true
1.8 原始封装类型
原始类型有三种:String,Number,Boolean
当读取字符串、数字或布尔值时,原始封装类型将被自动创建
var name = "Nicholas";
var firstChar = name.charAt(0);
console.log(firstChar); // "N"
// 这是在背后发生的事情如下。
var name = "Nicholas";
var temp = new String(name);
var firstChar = temp.charAt(0);
temp = null;
console.log(firstChar); // "N"
你可以在任何时候给一个真的对象添加属性,属性会保留至你手动删除它们。原始封装类型的属性会消失是因为被添加属性的对象立刻就被销毁了
由于第二行把字符串当成对象使用,JavaScript 引擎创建了一个字符串的实体让 charAt(0)可以工作。字符串对象的存在仅用于该语句并在随后被销毁(一种称为自动打包的过程)。为了测试这一点,试着给字符串添加一个属性看看它是不是对象。
// 试图给字符串name添加last属性。代码运行时没有错误,但是属性却消失了
var name = "Nicholas";
name.last = "Zakas";
console.log(name.last); // undefined
// what the JavaScript engine does
var name = "Nicholas";
var temp = new String(name);
temp.last = "Zakas";
temp = null; //临时对象销毁
var temp = new String(name);
console.log(temp.last); // undefined
temp = null;
实际上是在一个立刻就被销毁的临时对象上而不是字符串上添加了新的属性。之后当你试图访问该属性时,另一个不同的临时对象被创建,而新属性并不存在。虽然原始封装类型会被自动创建,在这些值上进行 instanceof 检查对应类型的返回值却都是 false。这是因为临时对象仅在值被读取时创建。instanceof 操作符并没有真的读取任何东西,也就没有临时对象的创建,于是它告诉我们这些值并不属于原始封装类型。
var name = "Nicholas";
var count = 10;
var found = false;
console.log(name instanceof String); // false
console.log(count instanceof Number); // false
console.log(found instanceof Boolean); // false
你也可以手动创建原始封装类型,但有某些副作用。手动创建原始封装类型实际会创建出一个 object,这意味着 typeof 无法鉴别出你实际保存的数据的类型
var name = new String("Nicholas");
var count = new Number(10);
var found = new Boolean(false);
console.log(typeof name); // "object"
console.log(typeof count); // "object"
console.log(typeof found); // "object"
总结(重点)
JavaScript 中虽然没有类,但是有类型。每个变量或数据都有一个对应的原始类型或引用类型。5 种原始类型(字符串、数字、布尔、空类型以及未定义)的值会被直接保存在变量对象中。除了空类型,都可以用 typeof 来鉴别。空类型必须直接跟 null 进行比较才能鉴别。
引用类型是 JavaScript 中最接近类的东西,而对象则是引用类型的实例。可以用 new 操作符或字面形式创建新对象。通常可以用点号访问属性和方法,也可以用中括号。函数在 JavaScript 中也是对象,可以用 typeof 鉴别它们。至于其他引用类型,你应该用 instanceof 和一个构造函数来鉴别。
为了让原始类型看上去更像引用类型,JavaScript 提供了 3 种原始封装类型:String、Number 和 Boolean。JavaScript 会在背后创建这些对象使得你能够像使用普通对象那样使用原始值,但这些临时对象在使用它们的语句结束时就立刻被销毁。虽然你也可以自己创建原始封装类型的实例,但是它们太容易令人误解,所以最好别这么干。