从一个面试题目入手: 如何实现一个类?
1. 类的三种属性:
- 公有属性:使用__proto__访问的属性
- 私有属性: 实例自带的属性
- 静态方法 (es7中有了静态属性)
2. 利用构造函数的方式模拟类
// 父类
function Parent() {
// 构造函数中的this, 通过new调用的,那么this指代的是实例
// 私有属性
this.name = 'parent'
}
// 公有
Parent.prototype.eat = function () {
console.log('eat')
}
// 子类
function Child() {
this.age = 9
}
// 子类的公有属性
Child.prototype.smoking = function () {
console.log('smoking')
}
let parent = new Parent() // 父类实例
let child = new Child() // 子类实例
console.log(child.__proto__.constructor) // 指向构造函数Child
// 注意:
console.log(child.__proto__.constructor.age) // 这样是拿不到age属性的,因为child.__proto__.constructor得到的是构造函数,而age是定义在实例上的
继承方式:
- 继承私有属性:即child可以获取到parent的name属性,调用构造函数 Parent.call(this), 如上上述代码所示。
- 继承公有属性:即继承parent原型上的属性,child既要保留自己的属性smoking又可以继承父类的eat。
function Parent() {
this.name = 'parent'
}
Parent.prototype.eat = function () {
console.log('eat')
}
function Child() {
this.age = 9
Parent.call(this) // 继承私有属性
}
Child.prototype.smoking = function () {
console.log('smoking')
}
Child.prototype = Parent.prototype // 子类的原型指向了父类的原型
let parent = new Parent()
let child = new Child()
console.log(child.eat())
// console.log(child.smoking()) // 子类原本在原型上的属性就丢失了
// 给子类设置一个新的属性后,父类上也能访问到这个属性的值, 父类上修改,子类上也会修改, 没有继承关系了,这种方式是不对的!!!
Child.prototype.a = 100
console.log(parent.a)
正确继承父类的公有属性的方式是:
function Parent() {
this.name = 'parent'
}
Parent.prototype.eat = function () {
console.log('eat')
}
function Child() {
this.age = 9
Parent.call(this) // 继承私有属性
}
Child.prototype.smoking = function () {
console.log('smoking')
}
Child.prototype.__proto__ = Parent.prototype
// 可以实现,但是一般不会直接操作__proto__, 以后可以使用下面的这种等价写法
// Object.setPrototypeOf(Child.prototype, Parent.prototype)
let parent = new Parent()
let child = new Child()
child.eat()
console.log(Child.prototype.constructor) // [Function: Child]
另一种方法: Object.create()
, 这种方式就是只实现公有属性的继承,Child.prototype = Object.create(Parent.prototype)
,这种方式是如何实现的呢?代码如下:
// 如何实现的呢?
function Parent() {
this.name = 'parent'
}
Parent.prototype.eat = function () {
console.log('eat')
}
function Child() {
this.age = 9
Parent.call(this) // 继承私有属性
}
Child.prototype.smoking = function () {
console.log('smoking')
}
// 简单实现Object.create()
function create(parentPrototype) {
function Fn() { }
Fn.prototype = parentPrototype // Fn上只会有父类的公有属性
return new Fn()
}
Child.prototype = create(Parent.prototype)
let child = new Child()
console.log(child.constructor) // [Function: Parent] , 存在一个问题就是子类的constructor指向变成了父类
如何解决上述问题?如果使用Object.create()
来实现的话是通过传入参数对象来实现的:
Child.prototype = Object.create(Parent.prototype, {
constructor: {value: Child}
}) // 将constructor指回Child
console.log(child.constructor)
如何自己实现上述的constructor指向问题?
首先来看Object.defineProperty()
的用法:
let a = {}
Object.defineProperty(a, 'name', {
enumerable: true, // 表示这个属性可以被枚举出来,是可见的
configurable: true, // 是否可以被删除
// writable: true, // 是否可以修改, 跟set冲突 写一个就行
get() {
console.log('get')
return 'xy'
},
set(value) {
console.log('设置值')
}
})
console.log(a.name)
a.name = 'milly'
// 以上这种方式,在做双向绑定的时候会用到
现在来实现刚才create中的constructor指向问题:
function create(parentPrototype, props) {
function Fn() { }
Fn.prototype = parentPrototype
let fn = new Fn()
for (let key in props) {
Object.defineProperty(fn, key, {
...props[key],
enumerable: true
})
}
return fn
}
Child.prototype = Object.create(Parent.prototype, {
constructor: { value: Child }
})
let child = new Child()
console.log(child.constructor) // Child
还有一种既可以继承方式:
Child.prototype = new Parent() // 基本不用, 因为new出来的实例上既有父类的私有又有父类的公有
3. class创建类
首先使用class创建一个类:
class Child {
constructor () {
this.age = 9 // 私有属性
}
// 注意: 之间没有逗号
static a() { // 属于类上的方法,通过类来调用
return 1
}
smoking() { // 公有, 原型上的方法
console.log('sssss')
}
}
let child = new Child()
child.smoking()
console.log(Child.a()) // 通过类,调用静态方法
继承的写法:
class Parent {
constructor() {
this.name = 'parent'
}
static b() {
return 2
}
eat () {
console.log('eat')
}
}
class Child extends Parent{ // 要求继承父亲的私有和公有
constructor() {
super() // 等价于Parent.call(this) 继承父类的私有属性
this.age = 9
}
// 注意: 之间没有逗号
static a() { // 属于类上的方法,通过类来调用
return 1
}
smoking() { // 公有, 原型上的方法
console.log('sssss')
}
}
let child = new Child()
child.smoking()
child.eat() // 继承公有属性
console.log(child.name) // 继承私有属性
// 在父类上有一个静态方法b, 在子类上能否被继承呢? ---
// 一定要明确: 静态方法一定是属于类的,而不是实例的
console.log(Child.b()) // 可以继承
// 所以,类可以继承公有、私有和静态方法
父类的构造函数中返回一个引用类型的时候, 会把这个引用类型作为子类的this。
class Parent {
constructor() {
this.name = 'parent'
return {xx: 'xy'}
}
static b() {
return 2
}
eat() {
console.log('eat')
}
}
class Child extends Parent { // 要求继承父亲的私有和公有
constructor() {
super() // 等价于Parent.call(this) 继承父类的私有属性
this.age = 9
}
// 注意: 之间没有逗号
static a() { // 属于类上的方法,通过类来调用
return 1
}
smoking() { // 公有, 原型上的方法
console.log('sssss')
}
}
let child = new Child()
console.log(child) // { xx: 'xy', age: 9 } , 这样的话就没有父类的name属性了
小结:class创建类需要注意的点
- class类只能new
- es6只支持静态方法,没有静态属性, 静态方法是类上的方法,通过类来调用
- 类可以继承公有属性、 私有属性和静态方法
- 父类的构造函数中返回一个引用类型的时候,会把这个引用类型作为子类的this
4. 如何实现一个类?
- 实现一个没有继承关系的
实现思路:
- 首先,使用立即执行函数搭起一个类的框架:
let Parent = (function () {
// 写逻辑
function P() {
}
return P;
})();
- 判断是不是通过
new
调用的:
如何判断呢? 返回函数P中的this
如果是window,那就是直接调用的,但如果是P的实例,就是new出来的
// 类的调用检测, 检测实例是不是new出来的
function _classCallCheck(instance, constructor) {
if (!(instance instanceof constructor)) {
throw new Error('Class constructor Child cannot be invoked without "new"');
}
}
// 类的实现
let Parent = (function () {
// 写逻辑
function P() {
_classCallCheck(this, P) // 类调用检查
}
return P;
})();
Parent(); // 报错
let p = new Parent(); // 正常
- 私有属性很好设置,直接复制过来就可以了
// 类的调用检测, 检测实例是不是new出来的
function _classCallCheck(instance, constructor) {
if (!(instance instanceof constructor)) {
throw new Error('Class constructor Child cannot be invoked without "new"');
}
}
// 类的实现
let Parent = (function () {
// 写逻辑
function P() {
_classCallCheck(this, P) // 类调用检查
this.name = 'parent'
}
return P;
})();
let p = new Parent(); // 正常
- 设置公有方法和静态方法,这两者的区别在于,如果是给P的原型添加方法就是添加的公有方法,如果是给P类添那么添加的就是静态方法:
function _classCallCheck(instance, constructor) {
if (!(instance instanceof constructor)) {
throw new Error('Class constructor Child cannot be invoked without "new"');
}
}
// 创建类
function _createClass(constructor, protoProperties, staticProperties) {
// constructor --- 构造函数
// protoProperties --- 原型方法的描述, 数组类型
// staticProperties --- 静态方法的描述, 数组类型
if (protoProperties.length > 0) {
// 将属性添加到构造函数的原型上
defineProperties(constructor.prototype, protoProperties);
}
if (staticProperties.length > 0) {
// 将属性添加到构造函数上
defineProperties(constructor, staticProperties);
}
}
let Parent = (function () {
function P() {
_classCallCheck(this, P) // 类调用检查
this.name = 'parent'
}
// 创建类
_createClass(
P,
[
{
key: "eat",
value: function () {
console.log("eat");
}
}
],
[
{
key: "b",
value: function () {
return 2
}
}
]
);
return P;
})();
- 接下来就是
defineProperties()
方法的实现,这里就要用到Object.defineProperty()
去给目标对象添加方法了:
defineProperties (target, properties) {
// target: 构造函数或者是构造函数的原型
// properties: 对象数组
for (let i=0; i<properties.length; i++) {
Object.defineProperty(target, properties[i].key, {
...properties[i],
configurable: true,
enumerable: true,
writable:true
})
}
}
- 现在来测试这个类,基本可以访问了:
let p = new Parent();
p.eat()
console.log(Parent.b())
- 手写实现类的继承
// 子类继承父类
function _inherits(subClass, superClass) {
// 继承公有属性
subClass.prototype = Object.create(superClass.prototype, {
constructor: {value:subClass}
})
// 继承父类的静态方法: 子类的链,连接到父类上,就可以调用父类的方法了
Object.setPrototypeOf(subClass, superClass)
}
接下来创建子类:
let Child = (function (Parent) {
// 实现继承父类的公有属性和静态方法
_inherits(C, Parent)
function C() {
_classCallCheck(this, C) // 类型检测
let obj = Parent.call(this) // 需要处理,父类返回一个引用类型的情况
let that = this
if (typeof obj === 'object') {
that = obj
}
// 设置子类的私有属性
that.age = 9
return that
}
return C
})(Parent);
到这里一个简单的继承就实现了。