面向对象编程(OOP)是一种编程范例,它的思想是它将真实世界各种复杂的关系,抽象为一个个对象。同时强调数据本身和数据行为在本质上是相关联的,所以会将属性和方法全部封装在对象内。
基于类的面向对象
传统的的面向对象 是类设计模式 抽象出类(Class),根据类(Class)创建实例(Instance)。这种基于类的面向对象本身就是一种底层设计模式-类模式。
在基于类的面向对象语言中有专门的类定义来定义一个类。类中定义的所有属性和方法都是实例的抽象表示而不是实例本身。
基于类的语言当中继承的实现也是 子类对父类的继承,继承后子类创建的实例具有父类的属性和方法。通过子类实例化的实例会复制父类上的方法。
同时鼓励在子类中直接使用同名方法重写父类中的方法。且子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
以上就是基于类实现的面向对象的封装、继承、多态
简言之,基于类的面向对象就是使用类作为实例对象的抽象表达来实现面向对象编程的特性。
然而,面向对象仅仅是一个概念或者编程思想而已,它不应该依赖于某个语言存在。类的机制只是实现面向对象编程的一种手段,而非必须。下面我们就看看js是如何实现面向对象的,这种实现机制又和类有什么区别。
基于原型的面向对象
原型模式
原型模式本身就是创建型设计模式的一种,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”。
说白了基于原型的面向对象,无需借助类,而是通过对象到对象之间的关联实现。
假如我们抛开我们认识到的 prototype ,单从原型模式的思路考虑我们创建一个对象。
那我们大概思路应该是,在语言内部定义一个包含Object通用属性的对象实例,当创建新对象的时候把这个实例复制到新的对象中。
js中无非就是把我们说的包含Object通用属性的对象实例放在prototype上而已。
原型属性存取进步历程
let obj1 = new Object()
let obj2 = {}
obj2.value = 1
最原始的构造时复制,每次构造时会开辟同等大小的内存空间。这虽然使得 obj1、obj2 与它们的原型完全一致,但也非常地不经济—内存空间的消耗会急速增加。
优化策略写时复制,我们只要在系统中指明 obj1 和 obj2 等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。当需要写对象(例如 obj2)的属性时,我们就复制一个原型的映象出来。不过对于经常写操作的系统来说,这种法子并不比 上一种法子经济。
优化-设定读取规则:这种方法的特点是:仅当写某个实例的成员时,将成员的信息添加到实例的映象中 。同时设定读取规则:
1、保证写入实例的属性在读取时被首先访问到。
2、在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空(null) 或找到该属性。
由构造过程我们了解到,JavaScript 中的对象实例本质上只是“一个指向 其原型的,并持有一个成员列表的结构”。
js中的原型
[[prototype]]
es标准:所有对象都有一个叫做 [[Prototype]] 的内部属性。此对象的值是 null 或一个对象,并且它用于实现继承。从 [[Prototype]] 对象继承来的命名数据属性(作为子对象的属性可见)是为了 get 请求,但无法用于 put 请求。 这个就是上图中红色部分-原型,指向要被复制的实例。
proto
上面说到,在js规范中对象原型是一个内部属性,内部属性是不可见且不可访问的。但是浏览器提供了一个proto来操作对象的原型。尽管proto还不是标准,但为了方便,我们还是会使用它来代表对象的原型。
prototype
es标准:构造函数的“ prototype ”属性的值是一个原型对象,用于实现继承和共享属性。
es标准: 函数原型对象的[[Prototype]]内部属性的值是标准的内置对象原型对象。
es标准:Object的原型对象的[[Prototype]] 内部属性为null。
function a(){}
a.prototype.__proto__ == Object.prototype => function.prototype = new Object()
// 标准里的意思就是 函数默认携带的 prototype 是一个空new Object()实例
Object.prototype.__proto__ == null
由标准可知,JavaScript 只有一种结构:对象。每个实例对象( object )都有一原型[[prototype]]( 浏览器中表现为proto )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,Object的原型对象的proto为null,就把它作为原型链的顶端。
尽管单独使用原型模式的继承有很多弊病(见下方继承实践中原型继承、原型链继承),例如:共享引用类型、无法向父类构造函数传参、处理构造函数中新增方法时要谨慎以免覆盖原型对象等。但是基于原型模式的面向对象依旧是十分强大的,相比基于类的面向对象来说不仅少了脆弱基类的弊端,也更加灵活、更适应动态的js语言。
基于类和基于原型的对象系统的比较
基于类 | 基于原型 |
---|---|
类和实例不同 | 所有对象均为实例 |
通过类定义来定义类,通过构造器方法来实例化类 | 通过构造器函数来定义和创建一组对象。 |
通过类定义来定义现存类的子类,从而构建对象的层级结构。 | 指定一个对象作为原型并且与构造函数一起构建对象的层级结构 |
遵循类链继承属性。 | 遵循原型链继承属性 |
类定义指定类的所有实例的所有属性。无法在运行时动态添加属性 | 构造器函数或原型指定实例的初始属性集。允许动态地添加或移除属性 |
js中继承实践
面向对象中说到的最多的一直都是继承,而js中继承的实践在红宝书中写的足够详细和优秀,所以下面基本算是搬了一下里面的内容,此处不再多说。
原型链继承
function Parent () {
this.name = 'izk';
}
Parent.prototype.getName = function () {
return this.name
}
function Child () {}
Child.prototype = new Parent();
let child1 = new Child();
console.log(child1.getName()) // izk
缺点:1、无法向父构造函数传参 2、引用类型的实例被共享
盗用构造函数继承
function Parent (name) {
this.name = name
}
function Child (name) {
Parent.call(this,name);
}
let child1 = new Child('izk');
let child2 = new Child('izk2');
console.log(child2.name);
优点:1、可以传参 2、不用共享属性
缺点: 子类不能访问父类原型,必须在构造函数中创建一遍方法,函数不能重用。
组合继承
function Parent (name) {
this.name = name;
this.title = '正常人'
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let child1 = new Child('izk', '18');
child1.title = '不太正常'
console.log(child1);
let child2 = new Child('izk2', '20');
console.log(child2);
综合了原型链继承和盗用构造函数继承双方的优点。
原型式继承和寄生继承
// 理解的时候抛开构造函数,直接想原型模式。本质就是原型指针指向一个对象。
//原型模式 // 本质和原型链是一样的,只不过是用不同的方式挂在对象的__proto__上。
animal = {
run(){console.log(run)},
name:'noName'
}
let dog = Object.create(animal);
function ObjCreate(prototype){
function F(){}
F.prototype = prototype
return new F()
}
let cat = ObjCreate(animal);
// 寄生模式
function createObj (prototype) {
var clone = Object.create(prototype);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
// 缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
寄生组合继承
function Parent (name) {
this.name = name;
this.title = '正常人'
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 红宝书把下面这两行封装成了一个函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
es6的class
我们都知道,基本上,ES6 的class
可以看作只是一个语法糖。那么es6的class到底会编译成什么内容?
extends 是如何实现?我们去bable中寻找答案。
constructor — 构造函数
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return 'hello, I am ' + this.name;
}
}
var Person = /*#__PURE__*/ (function () {
function Person(name) {
this.name = name;
}
var _proto = Person.prototype;
_proto.sayHello = function sayHello() {
return "hello, I am " + this.name;
};
return Person;
})();
上面转义后的代码可以看出,写在Class内部的普通方法实际是挂在构造函数的prototype对象上的,constructor函数则是构造函数本身。
static — 静态方法 只在类中存在的方法
// class
class Person {
static sayHello() {
return 'hello';
}
}
Person.sayHello() // 'hello'
let person = new Person();
person.sayHello(); // TypeError: person.sayHello is not a function
// es5
function Person() {}
Person.sayHello = function sayHello() {
return "hello";
};
extends — 继承
Class 通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,在代码上要清晰和方便。
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的 constructor(name)
this.age = age;
}
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype); // 利用Parent.prototype 提供 Child.prototype的__proto__
subClass.prototype.constructor = subClass; //
subClass.__proto__ = superClass; // Child的原型指针指向Parent,这是为了私有属性
}
var Parent = function Parent(name) {
this.name = name;
};
var Child = /*#__PURE__*/ (function (_Parent) {
_inheritsLoose(Child, _Parent);
function Child(name, age) {
var _this;
_this = _Parent.call(this, name) || this; // 调用父类的 constructor(name)
_this.age = age;
return _this;
}
return Child;
})(Parent);
由此可见class的继承语法源于寄生组合继承。但又比寄生组合继承多一步 Children.proto = Parent用于挂载私有属性。