ES6引入了一个很甜的语法糖就是 class, class 能够帮助开发者回归到 Java 时代的面向对象编程而不是 ES5 中被诟病的面向原型编程. 我也在工做的业务代码中大量的使用 class, 不多去触及 prototype 了.javascript
两面性:java
class 语法糖的本质仍是prototype, 因此应该回归到写 prototype 上.typescript
既然有 class 的写法了, 并且继承上也相比原型好写, 好理解许多, 因此应该向前看, 摒弃掉之前的 prototype 写法.编程
睿智而理性的读者, 你的理解是其中之一仍是两者兼备? 个人见解是: 语法糖存在即合理, 语法糖不只仅是更高层级的封装, 它能够避免写出没有语法糖时候的 bug, 可是语法糖不是语言自己的特性, 因此也必定要理解背后的成因, 加上原型是 JavaScript 里面特别特别重要的知识点, 不能不去深究. 你能够不用, 但不能不懂.数组
好了, 来看看 ES5 的面向对象编程函数
注意无序二字, 可理解为包含必定属性或方法的键值对. 是的, 本质上, 对象就是包含键值对映射的集合, 键是属性的名字, 值就是属性性能
数据属性this
访问器属性prototype
注意, 第二个访问器属性就是被著名的 React 和 Vue 实现数据响应的原理之一. 再注意, 以上全部的 bool 属性在没有进行配置的时候都默认为 false.code
在 configurable 为 true 的状况下, 凡是包含 value 或 writable 的会默认为数据属性, 会将原有的 get 和 set 属性删除, 反之若是设置了 get 或 set, 那么就会认为为访问器属性, 将 value 和 writable 删除
const o = {} Object.defineProperty(o, 'name', { configurable: true, enumerable: false, // 可不写, 默认为 false value: 'lorry', writable: false // 可不写, 默认为 false }) console.log(o)//{name: "lorry"} o.name = 'jiang'// 不会改变, 由于 writable 为 false console.log(o)//{name: "lorry"} Object.keys(o)// [] // 转化为访问器属性 o['_name'] = 'lorry'; // 设置私有属性 Object.defineProperty(o, 'name', { get: function(){return this._name}, set: function(newName){this._name = newName}, configurable: false, enumerable: true }) console.log(o); // {_name: "lorry"} o.name = 'setted jiang' console.log(o.name); // setted jiang Object.keys(o); // ["_name", "name"]
除了Object.defineProperty
以外, 还有其余的跟对象属性相关的原生方法
Object.defineProperties(o, {attr1:{}, attr2:{}})
, 批量设置一个对象多个属性
Object.getOwnPropertyDescriptor(o, attrName)
, 获取对象某个属性的配置
Object.getOwnPropertyDescriptors(o)
, 获取对象全部属性的配置
function createObject(name) { var o = new Object(); o.name = name; o.sayName = function() {console.log(this.name)}; return o; } const p1 = createObject('lorry')
优势: 简单直观 缺点: 没法进行对象识别, 没有 instanceof 能够去追溯.
function Person(name) { this.name = name; this.sayName = function() {console.log(this.name)}; } const p1 = new Person('lorry')
注意, 凡是构造函数都应该首字母大写 优势:
不显式建立对象(实质仍是有建立新对象)
使用 this 的上下文对象
不用 return(默认隐式建立的新对象)
可以使用 p1 instanceof Person
进行对象识别
缺点:
function Person() {}; Person.prototype.name = 'Lorry'; Person.prototype.sayName = function() { console.log(this.name); } const p1 = new Person(); const p2 = new Person(); p1.sayName(); // lorry; console.log(p1.sayName === p2.sayName) // true
能够看到两个实例p1 和 p2 共享同一个 name 属性和 sayName 的方法, 会节省内存.
注意, 在原型上的方法和属性是不会被 hasOwnProperty()
检测出来的(Object.keys()一样如此), 可是在in
中是有的.好比
p1.hasOwnProperty('name'); // false Object.keys(p1); // [] 'name' in p1; // true
一种更简单的定义方法
function Person(){}; Person.prototype = { name: 'lorry', sayName: function(){ console.log(this.name) }, //ES6 sayName2() { console.log(this.name) } }
这种方式彻底重写了 prototype, 包括其原有的 constructor 属性(指向了字面量对象即 Object)
解决办法就是手动指定一下
Person.prototype = { constructor: Person }
原型对象的问题:
实例没法给构造函数传值
共享既是优势也是缺点, 有些属性但愿各个实例各自保持本身的, 就没法经过此方法实现
看到了吗? 构造函数模式和原型模式实质上是两个极端, 一个是每一个实例都是各自为营, 一个是每一个实例都步调一致, 因此, 二者的结合就是更好的解决方案.也是如今最经常使用的方案.
function Person(name) { // 每一个实例各有的 this.name = name } // 每一个实例共享的 Person.prototype.sayName = function() { console.log(this.name) }
还有一种动态原型的变体
function Person(name) { this.name = name; // 只会在构造函数初始化时建立一次 if (typeof this.sayName !== 'function') { Person.prototype.sayName = function() { console.log(this.name) } } }
首先什么叫寄生? 以前我只知道这个模式叫寄生, 可是不知道为何叫寄生. 如今个人理解是: 寄生是一种相互独立的状态, 就像寄居蟹, 它能够爬到任何一个的壳中生活.看下面的例子
function Person(name) { const o = new Object(); o.name = name; o.sayName = function() { console.log(this.name) } return o; } const p1 = new Person('lorry') const p2 = Person('lorry') // p1和p2所拥有的属性和方法是同样的.
上述代码中, 壳就是 function Person(name){}
这部分, 寄居蟹就是剩余的部分, 调用 new Person()返回的对象跟 Person 没有任何原型上的关系(p1 instanceof Person = false
).
这样有什么好处呢? 私有变量
function Person(name) { const o = new Object() o.sayName = function() { console.log(name) } return o; } const p1 = new Person('lorry')
p1中就保存了一个稳定对象, 除了调用 sayName 以外没有任何办法能够获取到构造函数中的数据成员.
OO 的语言一般有两种继承
接口的继承, 只继承方法签名
实现的继承, 继承实际的方法
ECMA 只支持实现的继承, 也就是具体的方法, 固然一些JavaScript 的超集, 好比 typescript 能够支持接口的继承.
interface A { name: string } interface B extends A { age: number } var b: B = { name: 'lorry', age: 26 }
原理就是将 SubType 的[[ prototype ]] 属性指向了 SuperType 的prototype, 本质就是重写了 prototype.
function SuperType() { this.property = false; } SuperType.prototype.getSuperValue = function() { return this.property; } function SubType() { this.subProperty = true; } // 实现了原型继承, 拥有 SuperType 的全部实例属性和方法 SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function() { return this.subProperty; } const subIns = new SubType(); console.log(subIns.getSuperValue());
描述继承关系: SubType 继承 SuperType, SuperType 继承默认的原型 Object. 因此
console.log(subIns instanceof SubType) // true console.log(subIns instanceof SuperType) // true console.log(subIns instanceof Object) // true console.log(Object.prototype.isPrototypeOf(subIns))//true console.log(SuperType.prototype.isPrototypeOf(subIns))//true console.log(SubType.prototype.isPrototypeOf(subIns))//true
问题:
引用类型(好比数组)的原型属性会被全部实例共享.但实质上之因此在 SuperType 的构造函数中定义属性就是不但愿全部实例共享.
建立子类的实例时(上例中的 subIns ), 没法向父类构造函数中传参.由于继承不发生在构造函数中
为了解决上述的第二个问题, 有了这个构造函数继承方式
function SuperType(name) { this.name = name } function SubType(name) { SuperType.call(this, name); } const subIns = new SubType('lorry') subIns.name;// lorry
就跟构造函数的问题同样, 没法实现函数的复用.
跟组合建立对象模式同样, 将借用构造函数和原型链继承的方式组合起来就造成了组合继承的方式.
function SuperType(name) { this.name = name } SuperType.prototype.sayName = function () { console.log(this.name) } function SubType(name) { // 继承属性 SuperType.call(this, name); } // 继承方法 SubType.prototype = new SuperType() const subIns = new SubType('lorry') subIns.sayName() // lorry
注意: 其实在继承方法的时候也继承了实例的属性, 可是在查找原型链的时候, 由于实例自己就有其属性了, 不会再向上到超类中查找, 因此至关于只继承了方法. 这二者的结合就造成了最经常使用的继承方式.
这种方式是临时建立一个对象, 而后使该对象的原型指向超类, 最后返回该临时对象的实例. 因此该实例的 [[ prototype ]] 便指向了超类, 即相似对超类进行了一次浅复制.
function object(o) { function F(){}; F.prototype = o; return new F(); } const Person = { name: 'lorry', friends: ['A', 'B'] } const anotherPerson = object(Person); anotherPerson.name = 'Lourance' anotherPerson.friends.push('C') console.log(Person.name, Person.friends) // 'lorry', ['A', 'B', 'C']
上述的 object 函数就是 ES5 中的 Object.create()函数不传第二个参数的状况, 即Object.create(o)
等价于object(o)
还记得寄生吗? 就是那只寄居蟹.
function createAnother(origin) { const clone = object(origin) clone.sayName = function() { console.log(this.name) } return clone } const anotherPerson = createAnother(Person) anotherPerson.sayName() // lorry
这种方式有两个弊端:
以前说过最经常使用的继承模式为组合式继承, 可是组合式继承有一个问题就是会重复调用超类两次, 为了解决这个问题就可使用寄生组合式继承.
// 寄生模式 function inheritPrototype(child, parent) { const prototype = Object(parent.prototype); // 恢复 instanceof 原型链的追溯 prototype.constructor = child; child.prototype = prototype; } function SuperType(name) { this.name = name; this.friends = ['A', 'B'] } SuperType.prototype.sayName = function() { console.log(this.name) } function SubType(name, age) { SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); const subIns1 = new SubType('lorry', 26); const subIns2 = new SubType('lorry', 26); subIns1.sayName(); // lorry subIns1.friends.push('C'); console.log(subIns1.friends); // ['A', 'B', 'C'] console.log(subIns2.friends); // ['A', 'B']
上述就是最合理的继承方式, 集寄生式继承和组合继承的优势于一身. YUI 这个库就采用了上述的继承方式.
由此, 整个关于对象的内容就说完了.总结一下 建立对象的方式:
继承的方式: