读书摘要 -《JavaScript 面向对象精要》
图:书本封面
分享最近读的一本书 -《JavaScript 面向对象精要》,作者是尼古拉斯(Nicholas C. Zakas),著名的 JavaScript 开发者。你可能会因为他的另外一本书而熟悉-《JavaScript 高级程序设计》,熟称红宝书。作者文笔干练,每次都能用简洁、优雅的语言直击核心要点,另读者心领神会,就像这本书一样,短短 90 页内容,却贯穿了整个 JavaScript 最核心的部分。
这本书主要讲了,“如何在没有类概念的 JavaScript 中实现面向对象编程”,因为这本书的写作背景是在 2013 年,那时 JS 版本处于 ES5 阶段,还没有类的概念,书本的目标是指在帮助那些习惯使用带有类概念编程语言如 Java,C#的开发者快速上手 JS,同时也说明了 JS 是一门非常灵活的语言。
本书共有六个章节:
- 原始类型和引用类型
- 函数
- 理解对象
- 构造函数和原型对象
- 继承
- 对象模式
原始类型和引用类型
这章讲了 JS 的数据类型分为两类 - 原始类型和引用类型。
两者的区别:
原始类型 | 引用类型 | |
---|---|---|
存储方式 | 直接存储 | 只存储指针 |
鉴别方法 | typeof + “===null” | typeof function + instanceof |
另外,每个页面有自己的全局上下文-Array、Object 以及其他内建类型的版本,当你把一个数组从一个框架传到另一个框架的时候,instanceof 不能正常识别,因为那个数组是来自不同框架的 array 的实例,可以用 Array.isArray 解决这个问题
函数
函数在 JS 中是一等公民,因为它本质上也是一个对象,所以可以被当作对象使用,也可赋值给变量,也可作为函数的参数,或者作为函数的返回值,威力无穷大。
概要:
- 函数也是对象,和其它对象的区别就是内部具有[[Call]]属性
- 使用 typeof 操作符鉴别函数的时候,会检查是否具有内部属性[[Call]],如果找到,则返回”function”
- 函数声明会被提升到上下文的顶部,而函数表达式不会
- length 属性保存了期望的参数数量
- 函数重载的概念是一个函数可以具有多个签名,也就是可以声明多个同名函数,只是他们的参数数量,参数类型不同,然后调用的时候语言会自动根据提供的参数数量,参数类型调用相应的函数。由于 JS 同名函数会被覆盖,所以也就不能实现多个签名,也就没有重载的概念,JS 可以在函数内部通过判断参数的数量来实现伪重载。
- 所有的函数作用域内都有一个 this 对象代表调用该函数的对象
- 可以使用 this 对象来解耦合
理解对象
基于类的语言会根据类的定义锁定对象,JavaScript 的对象则没有这种限制,它是动态的,可在代码执行的任意时刻进行改变。
概要:
创建对象属性的时候,实际上会调用对象的内部属性[[Put]],修改的时候调用[[Set]],删除调用[[Delete]]
由于使用
if(obj.property){//do something}
判断属性是否存在的时候,当值为 null,undefined,NaN,false 或空字符串时评估为假,而对象的属性可能包含这些值,所以使用 in 操作符来判断属性是否存在更加靠谱。
in 操作符会检查实例属性和原型属性,可以配合 hasOwnProperty 来检查是否时自有属性for-in 和 Object.keys 都可以用来遍历对象的可枚举属性,后者只会在实例上遍历
Object.getOwnPropertyNames 可以用来获取对象的所有自身属性的属性名,包括不可枚举属性
- 对象的属性有两种类型,数据属性和访问器属性
两者的一些区别:
数据属性 | 访问器属性 | |
---|---|---|
存储 | 可以直接保存值 | 只定义 getter 和 setter 的行为 |
属性特征 | [[Enumerable]] [[Configurable]] [[Writable]] [[Value]] | [[Enumerable]] [[Configurable]] [[Get]] [[Set]] |
因为这些不同的特性,区别开了数据属性和防问器属性的工作模式
- 可以用 Object.defineProperty 或 Object.defineProperties 来更改属性的特性
- 可以用 Object.getOwnPropertyDescriptor()获取属性特性
对象和属性一样具有指导其行为的内部特征,其中,用[[Extensible]]来表示对象是否可以被修改,默认是可扩展的,也就是可以添加新属性。
禁止修改对象的一些方法:
- 禁止扩展。使用 Object.preventExtensions()创建不可扩展的对象
- 对象封印。使用 Object.seal() 创建的对象其[[Extensible]][[Configurable]]都为 false
- 对象冻结。使用 Object.freeze() 创建的对象,其数据属性都为只读[[Extensible]][[Configurable]][[Writable]]都为 false
应在严格模式下使用冻结对象,这样修改就会报错
继承
Object.create 可以用来创建一个对象并为其指定[[Prototype]]的值
创建一个对象字面量的时候,会自动去继承 Object.Prototype
const obj = { title: 'hi~'};
// auto done by engine
Object.create(Object.prototype,{ title: {
value: 'hi~',
configurable:true,
enumerable:true,
writable:true,
}})
这章讲了 JS 实现继承的几种办法:
对象继承:因为当访问对象属性时,会有原型链查找机制,所以可以利用 Object.create 来指定[[Prototype]]的值,从而实现继承
原型对象链继承:定义在原型对象(x.Prototype)上的属性和方法会被其实例共享
构造函数继承:改写子类的 prototype,令其[[Prototype]]属性指向父类的原型对象,从而实现原型链继承;
有以下两种实现方式:
1. ChildClass.prototype = new ParentClass(); ChildClass.prototype.constructor = ChildClass;
2. ChildClass.prototype = Object.create(ParentClass.prototype,{constructor: { value:ChildClass }})
构造函数窃取:利用 call,apply 方法在子类中调用父类构造函数从而继承到父类的自有属性,再通过修改子类 prototype 中的[[Prototype]]属性指向父类的 prototype 来实现继承父类原型对象上的方法,这也叫伪类继承
混入:不改变原型对象链,而是直接复制另一对象的属性,如果对象的属性也是一个对象,那就是浅拷贝
另外,JS 不提供 super 来调用父类的方法(ES5),可以用 call,apply 代替
如:
ParentClass.prototype.sayName.call(childClassThis);
对象模式
私有成员和特权成员 也就是怎么实现 OO 中的 private。JavaScript 对象的所有属性都是公开的,可以用下面两种方式实现私有属性:
模块模式
利用 IIFE 产生一个可以访问一个已销毁函数作用域的闭包,然后在返回的闭包里包含访问私有成员的“特权方法”。比如,
const obj = (function(){ let age = 18; return { getAge() { return age; }, setAge(val) { age = val; } } }())
构造函数的私有成员
在构造函数中使用本地变量,然后暴露更改变量的接口函数。比如,
function Person() { let age = 18; this.getAge = function() { return age; } this.setAge = function(val) { age = val; } }
作用域安全的构造函数当你没有使用 new 操作符去调用的时候,会在内部自己添加 new。比如,内置的 Array,Object,RegExp 和 Error
function Person(name){
if(this instanceof Person) {
// called with new
this.name = name;
} else {
// called without new
return new Person(name);
}
}
以上,尼古拉斯使用 JS 实现了 OOP 中类的:封装,聚合,继承和多态的特性。