"面向对象" 是以 "对象" 为中心的编程思想,它的思惟方式是构造。javascript
"面向对象" 编程的三大特色:"封装、继承、多态”:html
"面向对象" 编程的核心,离不开 "类" 的概念。简单地理解下 "类",它是一种抽象方法。经过 "类" 的方式,能够建立出多个具备相同属性和方法的对象。前端
可是!可是!可是JavaScript中并无 "类" 的概念,对的,没有。java
ES6 新增的 class
语法,只是一种模拟 "类" 的语法糖,底层机制依旧不能算是标准 "类" 的实现方式。git
在理解JavaScript中如何实现 "面向对象" 编程以前,有必要对JavaScript中的对象先做进一步地了解。es6
对象是"无序属性"的集合,表现为"键/值对"的形式。属性值可包含任何类型值(基本类型、引用类型:对象/函数/数组)。github
有些文章指出"JS中一切都是对象",略有偏颇,修正为:"JS中一切引用类型都是对象"更为稳妥些。编程
函数 / 数组都属于对象,数组就是对象的一种子类型,不过函数稍微复杂点,它跟对象的关系,有点"鸡生蛋,蛋生鸡"的关系,可先记住:"对象由函数建立"。segmentfault
new
操做符调用 Object
函数// 字面量 let person = { name: '以乐之名' }; // new Object() let person = new Object(); person.name = '以乐之名';
以上两种建立对象的方式,并不具有建立多个具备相同属性的对象。设计模式
TIPS:new
操做符会对全部函数进行劫持,将函数变成构造函数(对函数的构造调用)。
.
操做符访问 (也称 "键访问")[]
操做符访问(也称 "属性访问").
操做符 VS []
操做符:.
访问属性时,属性名需遵循标识符规范,兼容性比 []
略差;[]
接受任意UTF-8/Unicode字符串做为属性名;[]
支持动态属性名(变量);[]
支持表达式计算(字符串链接 / ES6的Symbol
)TIPS: 标识符命名规范 —— 数字/英文字母/下划线组成,开头不能是数字。
// 任意UTF-8/Unicode字符串做为属性名 person['$my-name']; // 动态属性名(变量) let attrName = 'name'; person[attrName]; // 表达式计算 let attrPrefix = 'my_'; person[attrPrefix + 'name']; // person['my_name'] person[Symbol.name]; // Symbol在属性名的应用
ES5新增 "属性描述符",可针对对象属性的特性进行配置。
Configurable
可配置(可删除)?[true|false]
Enumerable
可枚举 [true|false]
Writable
可写? [true|false]
Value
值?默认undefined
Get [[Getter]]
读取方法Set [[Setter]]
设置方法
writeable/value
get()
,会忽略其 value
值,直接调用 get()
;set()
,会忽略 writable
的设置,直接调用 set()
;set()
制定逻辑修改属性值)Object.defineProperty()
定义单个属性Object.defineProperties()
定义多个属性let Person = {}; Object.defineProperty(Person, 'name', { writable: true, enumerable: true, configurable: true, value: '以乐之名' }); Person.name; // 以乐之名
TIPS:使用 Object.defineProperty/defineProperties
定义属性时,属性特性 configurable/enumerable/writable
值默认为 false
,value
默认为 undefined
。其它方式建立对象属性时,前三者值都为 true
。
可以使用Object.getOwnPropertyDescriptor()
来获取对象属性的特性描述。
JavaScript中模拟 "面向对象" 中 "类" 的实现方式,是利用了JavaScript中函数的一个特性(属性)——prototype
(自己是一个对象)。
每一个函数默认都有一个 prototype
属性,它就是咱们所说的 "原型",或称 "原型对象"。每一个实例化建立的对象都有一个 __proto__
属性(隐式原型),它指向建立它的构造函数的 prototype
属性。
let Person = function(name, age) { this.name = name; this.age = age; }; Person.prototype.say = function() {}; let father = new Person('David', 48); let mother = new Person('Kelly', 46);
new
操做符的执行过程,会对实例对象进行 "原型关联",或称 "原型连接"。
__proto__
会指向函数的prototype
)”this
会指向这个新对象,并对this
属性进行赋值return
,通常不会有return
)"对象由函数建立",既然 prototype
也是对象,那么它的 __proto__
原型链上应该还有属性。Person.prototype.__proto__
指向 Function.prototype
,而Function.prototype.__proto__
最终指向 Object.prototype
。
TIPS:Object.prototype.__proto__
指向 null
(特例)。
平常调用对象的 toString()/valueOf()
方法,虽然没有去定义它们,但却能正常使用。实际上这些方法来自 Object.prototype
,全部普通对象的原型链最终都会指向 Object.prototype
,而对象经过原型链关联(继承)的方式,使得实例对象能够调用 Object.prototype
上的属性 / 方法。
访问一个对象的属性时,会先在其基础属性上查找,找到则返回值;若是没有,会沿着其原型链上进行查找,整条原型链查找不到则返回 undefined
。这就是原型链查找。
判断对象基础属性中是否有该属性,基础属性返回 true
。
for...in...
遍历对象全部可枚举属性in
判断对象是否拥有该属性Object.keys(...)
返回全部可枚举属性Object.getOwnPropertyNames(...)
返回全部属性修改对象属性时,若是属性名与原型链上属性重名,则在实例对象上建立新的属性,屏蔽对象对原型属性的使用(发生屏蔽属性)。屏蔽属性的前提是,对象基础属性名与原型链上属性名存在重名。
set()
,调用 set()
建立多个具备相同属性的对象
function createPersonFactory(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); } } var father = createPersonFactory('David', 48); var mother = createPersonFactory('Kelly', 46); father.say(); // 'My name is David, i am 48' mother.say(); // 'My name is Kelly, i am 46'
缺点:
say
方法没有共用内存空间obj.say = function(){...}
实例化一个对象时都会开辟新的内存空间,去存储function(){...}
,形成没必要要的内存开销。
father.say == mother.say; // false
new
)function Person(name, age) { this.name = name; this.age = age; this.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); } } let father = new Person('David', 48);
缺点:属性值为引用类型(say
方法)时没法共用,不一样实例对象的 say
方法没有共用内存空间(与工厂模式同样)。
function Person() {} Person.prototype.name = 'David'; Person.prototype.age = 48; Person.prototype.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); }; let father = new Person();
优势:解决公共方法内存占用问题(全部实例属性的 say
方法共用内存)
缺点:属性值为引用类型时,因内存共用,一个对象修改属性会形成其它对象使用属性发生改变。
Person.prototype.like = ['sing', 'dance']; let father = new Person(); let mother = new Person(); father.like.push('travel'); // 引用类型共用内存,一个对象修改属性,会影响其它对象 father.like; // ['sing', 'dance', 'travel'] mother.like; // ['sing', 'dance', 'travel']
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); }
原理:结合构造函数和原型的优势,"构造函数初始化属性,原型定义公共方法"。
构造函数 + 原型的组合方式,区别于其它 "面向对象" 语言的声明方式。属性方法的定义并无统一在构造函数中。所以动态原型建立对象的方式,则是在 "构造函数 + 原型组合" 基础上,优化了定义方式(区域)。
function Person(name, age) { this.name = name; this.age = age; // 判断原型是否有方法,没有则添加; // 原型上的属性在构造函数内定义,仅执行一次 if (!Person.prototype.say) { Person.prototype.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); } } }
优势:属性方法统一在构造函数中定义。
除了以上介绍的几种对象建立方式,此外还有"寄生构造函数模式"、"稳妥构造函数模式"。平常开发较少使用,感兴趣的伙伴们可自行了解。
传统的面向对象语言中,"类" 继承的原理是 "类" 的复制。但JavaScript模拟 "类" 继承则是经过 "原型关联" 来实现,并非 "类" 的复制。正如《你不知道的JavaScript》中提出的观点,这种模拟 "类" 继承的方式,更像是 "委托",而不是 "继承"。
如下列举JavaScript中经常使用的继承方式,预先定义两个类:
// 父类统必定义 function Person(name, age) { // 构造函数定义初始化属性 this.name = name; this.age = age; } // 原型定义公共方法 Person.prototype.eat = function() {}; Person.prototype.sleep = function() {};
// 原型继承 function Student(name, age, grade) { this.grade = grade; }; Student.prototype = new Person(); // Student原型指向Person实例对象 Student.prototype.constructor = Student; // 原型对象修改,须要修复constructor属性 let pupil = new Student(name, age, grade);
子类的原型对象为父类的实例对象,所以子类原型对象中拥有父类的全部属性
// 构造函数继承 function Student(name, age, grade) { Person.call(this, name, age); this.grade = grade; }
调用父类构造函数,传入子类的上下文对象,实现子类参数初始化赋值。仅实现部分继承,没法继承父类原型上的属性。可 call
多个父类构造函数,实现多继承。
属性值为引用类型时,需开辟多个内存空间,多个实例对象没法共享公共方法的存储,形成没必要要的内存占用。
// 原型 + 构造函数继承 function Student(name, age, grade) { Person.call(this, name, age); // 第一次调用父类构造函数 this.grade = grade; } Student.prototype = new Person(); // 第二次调用父类构造函数 Student.prototype.constructor = Student; // 修复constructor属性
结合原型继承 + 构造函数继承二者的优势,"构造函数继承并初始化属性,原型继承公共方法"。
父类构造函数被调用了两次。
待优化:父类构造函数第一次调用时,已经完成父类构造函数中** "属性的继承和初始化",第二次调用时只须要 "继承父类原型属性"** 便可,无须再执行父类构造函数。
// 寄生组合式继承 function Student(name, age, grade) { Person.call(this, name, age); this.grade = grade; } Student.prototype = Object.create(Person.prototype); // Object.create() 会建立一个新对象,该对象的__proto__指向Person.prototype Student.prototype.constructor = Student; let pupil = new Student('小明', 10, '二年级');
建立一个新对象,将该对象原型关联至父类的原型对象,子类 Student
已使用 call
来调用父类构造函数完成初始化,因此只需再继承父类原型属性便可,避免了经典组合继承调用两次父类构造函数。(较完美的继承方案)
class Person { constructor(name, age) { this.name = name; this.grade = grade; } eat () { //... } sleep () { //... } } class Student extends Person { constructor (name, age, grade) { super(name, age); this.grade = grade; } play () { //... } }
优势:ES6提供的 class
语法使得类继承代码语法更加简洁。
Object.create()
方法会建立一个新对象,使用现有对象来提供新建立的对象的__proto__
Object.create
实现的实际上是"对象关联",直接上代码更有助于理解:
let person = { eat: function() {}; sleep: function() {}; } let father = Object.create(person); // father.__proto__ -> person, 所以father上有eat/sleep/talk等属性 father.eat(); father.sleep();
上述代码中,咱们并无使用构造函数 / 类继承的方式,但 father
却可使用来自 person
对象的属性方法,底层原理依赖于原型和原型链的魔力。
// Object.create实现原理/模拟 Object.create = function(o) { function F() {} F.prototype = o; return new F(); }
Object.create(...)
实现的 "对象关联" 的设计模式与 "面向对象" 模式不一样,它并无父类,子类的概念,甚至没有 "类" 的概念,只有对象。它倡导的是 "委托" 的设计模式,是基于 "面向委托" 的一种编程模式。
文章篇幅有限,仅做浅显了解,后续可另开一章讲讲 "面向对象" VS "面向委托",孰优孰劣,说一道二。
instanceof
只能处理对象与函数的关系判断。instanceof
左边是对象,右边是函数。判断规则:沿着对象的 __proto__
进行查找,沿着函数的 prototype
进行查找,若是有关联引用则返回 true
,不然返回 false
。
let pupil = new Student(); pupil instanceof Student; // true pupil instanceof Person; // true Student继承了Person
Object.prototype.isPrototyepOf(...)
能够识别对象与对象,也能够是对象与函数。
let pupil = new Student(); Student.prototype.isPrototypeOf(pupil); // true
判断规则:在对象 pupil
原型链上是否出现过 Student.prototype
, 若是有则返回 true
, 不然返回 false
ES6新增修改对象原型的方法:Object.setPrototypeOf(obj, prototype)
,存在有性能问题,仅做了解,更推荐使用 Object.create(...)
。
Student.prototype = Object.create(Person.prototype); // setPrototypeOf改写上行代码 Object.setPrototypeOf(Student.prototype, Person.prototype);
"面向对象" 是程序编程的一种设计模式,具有 "封装,继承,多态" 的特色,在ES6的 class
语法未出来以前,原型继承确实是JavaScript入门的一个难点,特别是对新入门的朋友,理解起来并不友好,模拟继承的代码写的冗余又难懂。好在ES6有了 class
语法糖,没必要写冗余的类继承代码,代码写少了,眼镜片都亮堂了。
老话说的好,“会者不难”。深刻理解面向对象,原型,继承,对往后代码能力的提高及编码方式优化都有益处。好的方案不仅有一种,明白个中原因,带你走进新世界大门。
参考文档:
本文首发Github,期待Star!
https://github.com/ZengLingYong/blog
做者:以乐之名 本文原创,有不当的地方欢迎指出。转载请指明出处。