全方位理解JavaScript面向对象

JavaScript面向对象程序设计

本文会碰到的知识点:
原型、原型链、函数对象、普通对象、继承

读完本文,可以学到

  • 面向对象的基本概念
  • JavaScript对象属性
  • 理解JavaScript中的函数对象与普通对象
  • 理解prototype和proto
  • 理解原型和原型链
  • 详解原型链相关的Object方法
  • 了解如何用ES5模拟类,以及各种方式的优缺点
  • 了解如何用ES6实现面向对象

目录

1. 面向对象的基本概念

面向对象也即是OOP,Object Oriented Programming,是计算机的一种编程架构,OOP的基本原则是计算机是由子程序作用的单个或者多个对象组合而成,包含属性和方法的对象是类的实例,但是JavaScript中没有类的概念,而是直接使用对象来实现编程。
特性:

  • 封装:能够将一个实体的信息、功能、响应都封装到一个单独对象中的特性。

    由于JavaScript没有public、private、protected这些关键字,但是可以利用变量的作用域来模拟public和private封装特性

var insObject = (function() {
    var _name = 'hello'; // private
    return {
        getName: function() { // public
            return _name; 
        }
    }
})();


insObject._name; // undefined
insObject.getName(); // hello

这里只是实现了一个简单的版本,private比较好的实现方式可以参考深入理解ES6 145页
protected可以利用ES6的Symbol关键字来实现,这里不展开,有兴趣可以讨论

  • 继承:在不改变源程序的基础上进行扩充,原功能得以保存,并且对子程序进行扩展,避免重复代码编写,后面的章节详细描述
  • 多态:允许将子类类型的指针赋值给父类类型的指针;原生JS是弱类型语言,没有多态概念

    但是JavaScript也不是不能实现多态的概念,只是如果你之前是学静态语言的同学,理解起来可能有些误差。例子:

    比如我们有台电脑mac, 它有一个方法system来获取系统

    var mac = {
        system: function(){
           console.log('mac');
        }
    }
    
    var getSystem = function() {
        mac.system();  
    }
    
    getSystem();// mac

    某一天我们换成win,为了防止后面又换成mac,我们让getSystem函数有一定的弹性。

     var mac = {
      system: function(){
           console.log('mac');
       }
     }
    
     var win = {
       system: function(){
           console.log('win');
       }
     }
    
     var getSystem = function(type) {
       if (type == 'mac') {
           mac.system();
       } else if (type == 'win') {
           win.system();
       }
     }
    
     getSystem('mac');// mac
     getSystem('win');// win

    但是很明显这个函数还是有问题,某天我又换成centos呢。。。。我们改写一下getSystem这个函数

    var getSystem = function(ins) {
        if (ins.system instanceOf Function) {
            ins.system();
        }
    }

    这里我们是假设每个系统获取系统的名称都是system,实际开发过程中可能不会这样,这种情况可以用适配器模式来解决。

JavsScript中面向对象的一些概念:

  • 类class: ES5以前就是构造函数,ES6中有class
  • 实例instance和对象object:构造函数创建出来的对象一般称为实例instance
  • 父类和子类:JavaScript也可以称为父对象和子对象

2. JavaScript对象属性

想弄懂面向对象,是不是先看看对象是啥呢?
我们先看一个题目:

[] + {}; // "[object Object]"
{} + []; // 0

解释:
在第一行中,{}出现在+操作符的表达式中,因此被翻译为一个实际的值(一个空object)。而[]被强制转换为”“因此{}也会被强制转换为一个string:”[object Object]”。
但在第二行中,{}被翻译为一个独立的{}空代码块儿(它什么也不做)。块儿不需要分号来终结它们,所以这里缺少分号不是一个问题。最终,+ []是一个将[]明确强制转换 为number的表达式,而它的值是0

2.1 属性

对象的属性

  • Object.prototype Object 的原型对象,不是每个对象都有prototype属性
  • Object.prototype.proto 不是标准方法,不鼓励使用,每个对象都有proto属性,但是由于浏览器实现方式的不同,proto属性在chrome、firefox中实现了,在IE中并不支持,替代的方法是Object.getPrototypeOf()
  • Object.prototype.constructor:用于创建一个对象的原型,创建对象的构造函数

可能大家会有一个疑问,为什么上面那些属性要加上prototype
在chrome中打印一下var a = {}

属性描述符

数据属性:

特性名称 描述 默认值
value 属性的值 undfined
writable 是否可以修改属性的值,true表示可以,false表示不可以 true
enumerable 属性值是否可枚举,true表示可枚举for-in, false表示不可枚举 true
configurable 属性的特性是否可配置,表示能否通过delete删除属性后重新定义属性 true

例子:
这里写图片描述

访问器属性:

特性名称 描述 默认值
set 设置属性时调用的函数 undefined
get 写入属性时调用的函数 undefined
configurable 表示能否通过delete删除属性后重新定义属性 true
enumerable 表示能否通过for-in循环返回属性 true

访问器属性不能直接定义,一般是通过Object.defineProperty()方法来定义,但是这个方法只支持IE9+, 以前一般用两个非标准方法来实现__defineGetter__()֖__defineSetter__()
例子:

var book = { _year: 2004, edition: 1 };


Object.defineProperty(book, "year", { 
    get: function(){ 
        return this._year; 
    }, 
    set: function(newValue){
        if (newValue > 2004){ 
            this._year = newValue; 
            this.edition += newValue - 2004; 
        }
    }
});


book.year = 2005; 
alert(book.edition);

2.2 方法

  • Object.prototype.toString() 返回对象的字符串表示
  • Object.prototype.hasOwnProperty() 返回一个布尔值,表示某个对象是否含有指定的属性,而且此属性非原型链继承,也就是说不会检查原型链上的属性
  • Object.prototype.isPrototypeOf() 返回一个布尔值,表示指定的对象是否在本对象的原型链中
  • Object.prototype.propertyIsEnumerable() 判断指定属性是否可枚举
  • Object.prototype.watch() 给对象的某个属性增加监听
  • Object.prototype.unwatch() 移除对象某个属性的监听
  • Object.prototype.valueOf() 返回指定对象的原始值
  • 获取和设置属性
    • Object.defineProperty 定义单个属性
    • Object.defineProperties 定义多个属性
    • Object.getOwnPropertyDescriptor 获取属性
  • Object.assign() 拷贝可枚举属性 (ES6新增)
  • Object.create() 创建对象
  • Object.entries() 返回一个包含由给定对象所有可枚举属性的属性名和属性值组成的 [属性名,属性值] 键值对的数组,数组中键值对的排列顺序和使用for…in循环遍历该对象时返回的顺序一致
  • Object.freeze() 冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。也就是说,这个对象永远是不可变的。该方法返回被冻结的对象
  • Object.getOwnPropertyNames() 返回指定对象的属性名组成的数组
  • Object.getPrototypeOf 返回该对象的原型
  • Object.is(value1, value2) 判断两个值是否是同一个值 (ES6 新增)
  • Object.keys() 返回一个由给定对象的所有可枚举自身属性的属性名组成的数组,数组中属性名的排列顺序和使用for-in循环遍历该对象时返回的顺序一致
  • Object.setPrototypeOf(obj, prototype) 将一个指定的对象的原型设置为另一个对象或者null
  • Object.values 返回一个包含指定对象所有的可枚举属性值的数组,数组中的值顺序和使用for…in循环遍历的顺序一样

2.3 应用

  • 如何检测某个属性是否在对象中?

    • in运算符,判断对象是否包含某个属性,会从对象的实例属性、继承属性里进行检测
    function Dogs(name) {
        this.name = name
    }
    
    function BigDogs(size) {
        this.size = size;
    }
    
    BigDogs.prototype = new Dogs();
    
    var a = new BigDogs('big');
    
    'size' in a;
    'name' in a;
    'age' in a;
    • Object.hasOwnProperty(),判断一个对象是否有指定名称的属性,不会检查继承属性
    a.hasOwnProperty('size');
    a.hasOwnProperty('name');
    a.hasOwnProperty('age');
    • Object.propertyIsEnumerable(),判断指定名称的属性是否为实例属性并且是可枚举的
    // es6
    var a = Object.create({}, {
        name: {
            value: 'hello',
            enumerable: true,
        },
        age: {
            value: 11,
            enumerable: false,
        }
    });
    
    // es5
    var b = {};
    Object.defineProperties(b, {
        name: {
            value: 'hello',
            enumerable: true,
        },
        age: {
            value: 11,
            enumerable: false,
        } 
    });
    
    a.propertyIsEnumerable('name');
    a.propertyIsEnumerable('age');
  • 如何枚举对象的属性,并保证不同了浏览器中的行为是一致的?

    • for/in 语句,可以遍历可枚举的实例属性和继承属性
    var a = {
      supername: 'super hello',
      superage: 'super name',
    }
    var b = {};
    Object.defineProperties(b, {
      name: {
          value: 'hello',
          enumerable: true,
      },
      age: {
          value: 11,
          enumerable: false,
      } 
    });
    
    Object.setPrototypeOf(b, a); // 设置b的原型式a 等效的是b.__proto__ = a
    
    for(pro in b) {
      console.log(pro); // name, supername, superage
    }
    • Object.keys(), 返回一个数组,内容是对象可枚举的实例属性名称
     var propertyArray = Object.keys(b);
     // name
    • Object.getOwnPropertyNames(),返回一个数组,内容是对象所有实例属性,包括可枚举和不可枚举
     var propertyArray = Object.getOwnPropertyNames(b);
     // name, age
  • 如何判断两个对象是否相等?
    我只想说,这个问题说简单很简单,说复杂也挺复杂的传送门
    我们看个简单版的

    function isEquivalent(a, b) {
        var aProps = Object.getOwnPropertyNames(a);
        var bProps = Object.getOwnPropertyNames(b);
        if (aProps.length != bProps.length){
            return false;
        }
    
    
        for (var i = 0; i < aProps.length; i++) {
            var propName = aProps[i];
            if (a[propName] !== b[propName]) {
                return false;
            }
        }
        return true;
    }
    
    
    // Outputs: true
    console.log(isEquivalent({a:1},{a:1}));

    上面这个函数还有啥问题呢?

    • 没有对传入参数进行校验,例如判断是否是NaN,或者是其他内置属性
    • 没有判断传入对象的construct和prototype
    • 时间算法复杂度是O(n2)

    有同学可能会有疑问,能不能用Object.is,答案是否定的,Object.is简单来说就是在===的基础上特别处理了NaN,+0,-0,保证了-0和+0不相同,Object.is(NaN, NaN)返回true

  • 对象的深拷贝和浅拷贝
    其实如果大家理解了上面的那些方法,是很容易写出深拷贝和浅拷贝的代码的,我们先看一下这两者的却别。
    浅拷贝仅仅是复制引用,拷贝后a === b, 注意Object.assign方法实现的是浅复制(此处有深刻教训!!!)
    深拷贝这是创建了一个新的对象,然后把旧的对象中的属性和方法拷贝到新的对象中,拷贝后 a !== b
    深拷贝的实现由很多例子,例如jQuery的extend和lodash中的cloneDeep, clone。jQuery可以使用$.extend(true, {}, ...)来实现深拷贝, 但是jQuery无法复制JSON对象之外的对象,例如ES6引入的Map、Set等。而lodash加入的大量的代码来实现ES6新引入的标准对象
    这里需要单独研究分享/(ㄒoㄒ)/~~

3. 对象分为函数对象和普通对象

概念(什么是函数对象和普通对象)

Object、Function、Array、Date等js的内置对象都是函数对象

问题:

function a1 () {}
const a2 = function () {}
const a3 = new Function();


const b1 = {};
const b2 = new Object();


const c1 = [];
const c2 = new Array();


const d1 = new a1();
const d2 = new b1();????
const d3 = new c1();????


typeof a1;
typeof a2;
typeof a3;


typeof b1;
typeof b2;


typeof c1;
typeof c2;


typeof d1;

上面两行报错的原因,是因为构造函数只能由函数来充当,而b1和c1不是Function的实例,所以不能充当构造器

但是只有Function的实例都是函数对象、其他的实例都是普通对象

我们延伸一下,在看个例子

const e1 = function *(){};
const e2 = new e1();
// Uncaught TypeError: e1 is not a constructor
console.log(e1.constructor) // 是有值的。。。
// 规范里面就不能new
const e2 = e1();

GeneratorFunction是一个特殊的函数对象
e1.__proto__.__proto__ === Function.prototype

e1的原型实际上是一个生成器函数GeneratorFunction,也就是说
e1.__proto__ === GeneratorFunction.prototype

这行代码有问题么,啊哈哈哈,GeneratorFunction这个关键字主流的JavaScript还木有暴露出来,所以这个大家理解就好啦

虽然不能直接new e1
但是可以 new e1.constructor();哈哈哈哈

4. 理解prototype和proto

对象类型 prototype proto
函数对象 Yes Yes
普通对象 No Yes
  • 只有函数对象具有prototype这个属性
  • prototype__proto__都是js在定义一个对象时的预定义属性

  • prototype 被实例的__proto__指向

  • __proto__指向构造函数的prototype
const a = function(){}
const b = {}


typeof a // function
typeof b // object


typeof a.prototype // object
typeof a.__proto__ // function


typeof b.prototype // undefined
typeof b.__proto__ // object


a.__proto__ === Function.prototype
b.__proto__ === Object.prototype

理解了prototype__proto__之后,我们来看看之前一直说的为什么JavaScript里面都是对象

const a = {}
const b = function () {}
const c = []
const d = new Date()


a.__proto__
a.__proto__ === Object.prototype


b.__proto__
b.__proto__ === Function.prototype


c.__proto__
c.__proto__ === Array.prototype


d.__proto__
d.__proto__ === Date.prototype


Object.prototype.__proto__ //null


Function.prototype.__proto__ === Object.prototype


Array.prototype.__proto__ === Object.prototype


Date.prototype.__proto__ === Object.prototype

延伸一个问题:如何判断一个变量是否是数组?

  • typeof

我们上面已经解释了,这些都是普通对象,普通对象是没有prototype的,他们typeof的值都是object

typeof []
typeof {}
  • 从原型来看, 原理就是看Array是否在a的原型链中

a的原型链是 Array->Object

const a = [];
Array.prototype.isPrototypeOf(obj);
  • instanceof
const a = [];
a instanceof Array

从构造函数入手,但是这个方法和上面的方法都有一问题,不同的框架中创建的数组不会相互共享其prototype属性

  • 根据对象的class属性,跨原型调用tostring方法
const a = [];
Object.prototype.toString.call(a);
// [Object Array]

ES5 中所有内置对象的[[Class]]属性的值是由规范定义的,但是 ES6 中已经没有了[[Class]]属性,取代它的是[[NativeBrand]]属性,这个大家有兴趣可以自行去查看规范
原理:
1. 如果this的值为undefined,则返回”[object Undefined]”.
2. 如果this的值为null,则返回”[object Null]”.
3. 让O成为调用ToObject(this)的结果.
4. 让class成为O的内部属性[[Class]]的值.
5. 返回三个字符串”[object “, class, 以及 “]”连接后的新字符串.

问题?这个一定是正确的么?不正确为啥?
提示ES6的Symbol属性

  • Array.isArray()
    部分浏览器中不兼容

桌面浏览器
这里写图片描述
移动端浏览器
这里写图片描述

5. 理解原型与原型链

其实上一节中的prototype和proto就是为了构建原型链而存在的,之前也或多或少的说到了原型链这个概念。

看下面的代码:

const Dogs = function(name) {
    this.name = name;
}


Dogs.prototype.getName = function() {
    return this.name
}


const jingmao = new Dogs('jingmao');
console.log(jingmao);
console.log(jingmao.getName());

这段代码的执行过程
1.首先创建了一个构造函数Dogs,传入一个参数name,Dogs.prototype也会自动创建
2.给对象dogs增加了一个方法
3.通过构造函数Dogs实例化了一个对象jingmao
4.输出jingmao的值
这里写图片描述
可以看到jingmao有两个值name和proto,其中proto指向Dogs.prototype
5.执行getName方法时,在jingmao中找不到这个方法,就会继续向着原型链继续往上找,也就是通过proto,然后就找到了getName方法。

这个过程实际上就是原型继承,实际上JavaScript的原型继承就是利用了proto并借助prototype来实现的。

试一试下面 看输出结果是啥?

jingmao.__proto__ === Function.prototype


Dogs.prototype 指向什么
Dogs.prototype.__proto__ 指向什么
Dogs.prototype.__proto__.__proto__ 指向什么

上面例子中getName 最终是查找到了,那么如果在原型链中一直没查找到,会怎么样?
例如console.log(jingmao.age)

jingmao 是一个对象可以继续
jingmao.age 不存在,继续
jingmao.__proto__ 是一个对象可以继续
jingmao.__proto__.age 不存在,继续
jingmao.__proto__.__proto__ 是个对象可以继续
jingmao.__proto__.__proto__.age 不存在,继续
jingmao.__proto__.__proto__.__proto__ null,不是对象,到头啦

原型链的概念其实不重要,重要的是要理解,简单来说,原型链就是利用原型让一个引用类型继承另一个应用类型的属性和方法。

最后我们用一张图来结束本节
这里写图片描述

Array.__proto__ === Function.prototype
Object.__proto__ === Function.prototype

还有三点需要注意的:

  • 任何内置函数对象(类)本身的 _proto_都指向 Function 的原型对象;
  • 除了 Object 的原型对象的_proto_ 指向 null,其他所有内置函数对象的原型对象的 _proto_ 都指向 object。
  • 所有构造函数的的prototype方法的proto都指向Object.prototype(除了….Object.prototype自身)

如果理解了上面这些内容,大家可以自行描述一下,构造函数、原型和实例之间的关系,也可以举例说明

function Dogs (name) {
    this.name = name;
}


var jingmao = new Dogs('jingmao');

这个图大家脑子里面自己构想一下?

解释:
构造函数首字母必须大写,用来区分普通函数,内部使用this指针,指向要生成的实例对象,通过new来生成实例对象。
实例就是通过new一个构造函数产生的对象,它有一个属性[[prototype]]指向原型
原型中有一个属性[[constructor]],指向构造函数

6.与原型链相关的方法

这里只是简单介绍一下

6.1 hasOwnProperty

Object.hasOwnProperty() 返回一个布尔值,表示某个对象的实例是否含有指定的属性,而且此属性非原型链继承。用来判断属性是来自实例属性还是原型属性。类似还有in操作符,in操作符只要属性存在,不管实在实例中还是原型中,就会返回true。同时使用in和hasOwnProperty就可以判断属性是在原型中还是在实例中

const Dogs = function (age) {
    this.age = age
}


Dogs.prototype.getAge = function() {
    return this.age;
}


const jingmao = new Dogs(14);


jingmao.hasOwnProperty(age);

6.2 isPrototypeOf

Object.prototype.isPrototypeOf() 返回一个布尔值,表示指定的对象是否在本对象的原型链中

const Dogs = function (age) {
    this.age = age
}


Dogs.prototype.getAge = function() {
    return this.age;
}


const jingmao = new Dogs(11);
Object.prototype.isPrototypeOf(Dogs);
Dogs.prototype.isPrototypeOf(jingmao);

6.3 getPrototypeOf

Object.getPrototypeOf 返回该对象的原型

const Dogs = function (age) {
    this.age = age
}


Dogs.prototype.getAge = function() {
    return this.age;
}


const jingmao = new Dogs(11);


jingmao.__proto__ === Object.getPrototypeOf(jingmao) 

7. ES5 对象继承

7.1 原型继承

原型继承就是利用原型链来实现继承

function SuperType() {
    this.supername = 'super';
}


SuperType.prototype.getSuperName= function(){
    return this.supername;
}


function SubType () {
    this.subname='subname';
}


SubType.prototype = new SuperType();


SubType.prototype.getSubName = function (){
    return this.subname;
}


var instance1 = new SubType();
console.log(instance1.getSubName());
console.log(instance1.getSuperName());

这里写图片描述


需要注意的地方:
实现原型继承的时候不要使用对象字面量创建原型方法,因为这样做,会重写原型链。

function SuperType() {
    this.supername = 'super';
}


SuperType.prototype.getSuperName= function(){
    return this.supername;
}


function SubType () {
    this.subname='subname';
}


SubType.prototype = new SuperType();


SubType.prototype =  {
    getSubName: function (){
        return this.subname;
    }
}


var instance1 = new SubType();
console.log(instance1.getSubName());
console.log(instance1.getSuperName()); // error

这里写图片描述

上面使用SubType.prototype = {...}之后,SubType的原型就是Object了,而不是SuperType了。


优点:原型定义的属性和方法可以复用
缺点:
1. 引用类型的原型属性会被所有实例共享
2. 创建子对象时,不能向父对象的构造函数中传递参数

7.2 构造函数继承

这里的例子来源是JavaScript高级程序设计

在说构造函数继承之前,我们先看一个例子

var a = {
    name: 'a',
};


var name = 'window';


var getName = function(){
    console.log(this.name);
}


getName() // 输出window
getName.call(a) // 输出a

执行getName()时,函数体的this指向window,而执行getName.call(a)时,函数体的this指向的是a对象,所以就可以理解啦。接下来我们看如何实现构造函数继承

function SuperType () {
    this.colors = ['red', 'green'];
}


function SubType () {
    // 继承SuperType
    SuperType.call(this);
}


var instance1 = new SubType();
instance1.colors.push('blue'); 
console.log(instance1.colors); 
// red, green, blue


var instance2 = new SubType();
console.log(instance2.colors);
// red, green

SuperType.call(this); 这一行代码,实际上意思是在SubType的实例初始化过程中,调用了SuperType的构造函数,因此SubType的每个实例都有colors这个属性

优点:子对象可以传递参数给父对象。

function SuperType(name) {
    this.name = name;
}
function SubType(name, age) {
    name = name || 'hello';
    SuperType.call(this, name);
    this.age = age;
}


var instance1 = new SubType('scofield', 28);
console.log(instance1.name);
console.log(instance1.age);

需要注意的地方是在调用父对象的构造函数之后,再给子类型中的定义属性,否则会被重写。

缺点:方法都需要在构造函数中定义,难以做到函数的复用,而且在父对象的原型上定义的方法,对于子类型是不可见的。 ??? 为什么不可见

function SuperType(name) {
    this.name = name;
}


SuperType.prototype.getName = function() {
    return this.name;
}


SuperType.prototype.prefix = function() {
    return 'prefix';
}


function SubType(name) {
    SuperType.call(this, name);
}


var instance1 = new SubType('scofield');
console.log(instance1.name);
console.log(instance1.prefix);
console.log(instance1.getName());
// Uncaught TypeError: instance1.getName is not a function

7.2 组合式继承

组合式继承顾名思义,就是组合两种模式实现JavaScript的继承,借助原型链和构造函数来实现。这样子在原型上定义方法实现了函数的复用,而且能够保证每个实例都有自己的属性。

function SuperType (name) {
    this.name = name;
    this.con = [];
}


SuperType.prototype.getName = function() {
    return this.name;
}


function SubType (name, age) {
    SuperType.call(this, name);
    this.age = age;
}


SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.getAge = function() {
    return this.age;
};


var instance1 = new SubType('li', 18);
instance1.con.push('test1');
console.log(instance1.con); // test1
console.log(instance1.getAge()); // 18
console.log(instance1.getName()); // li


var instance2 = new SubType('hang', 18);
console.log(instance1.con); // test1
console.log(instance1.getAge()); // 18
console.log(instance1.getName()); // hang

优点:弥补了原型继承和构造函数的缺点
缺点:父类构造函数调用了两次

7.3 原型式继承

原型式继承并没有使用严格意义上的构造函数,借助原型可以基于已有的对象创建新的对象,例如:

function createObject(o) {
    function newOrient () {};
    newOrient.prototype = o;
    return new newOrient();
}

简单来说createObject函数,对传入的o对象进行的一次浅拷贝。在ES5中新增加了一个方法Object.create(), 它的作用和createObject是一样的,但是只支持IE9+。

var Dogs = {
    name: 'jingmao',
    age: 1
}


var BigDogs = Object.create(Dogs);
BigDogs.name= 'bigjingmao';
BigDogs.size = 'big';
console.log(BigDogs.age);

其中Object.create还支持传入第二个参数,参数与Object.defineProperties()方法的格式相同,并且会覆盖原型上的同名属性。

7.4 寄生式继承

寄生式继承其实和原型式继承很类似,区别在于,寄生式继承创建的一个函数把所有的事情做完了,例如给新的对象增加属性和方法。

function createAnother(o) {
    var clone = Object.create(o);
    clone.size = 'big';
    return clone;
}


var Dogs = {
    name: 'jingmao',
    age: 1
}


var BigDogs = createAnother(Dogs);
console.log(BigDogs.size);

7.5 寄生组合式继承

到最后一个了,看看我们之前遗留的问题:
组合继承会调用两次父对象的构造函数,并且父类型的属性存在两组,一组在实例上,一组在SubType的原型上。解决这个问题的方法就是寄生组合式继承。

function inheritPrototype(subType, superType){ 
    // 继承父类的原型
    var prototype = Object.create(superType.prototype);
    // 重写被污染的construct
    prototype.constructor = subType; 
    // 重写子类的原型  
    subType.prototype = prototype; 
}

这个函数就是寄生组合式继承的最简单的实现方式

function SuperType(name){ 
    this.name = name; 
    this.colors = ["red", "blue", "green"];
}


SuperType.prototype.sayName = function(){ 
    alert(this.name); 
};


function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}


inheritPrototype(SubType, SuperType);


SubType.prototype.sayAge = function(){ 
    alert(this.age); 
};


var instance1 = new SubType('hello', 18);


instance1.__proto__.constructor == SubType

这里写图片描述
可以看到
1. 子类继承了父类的属性和方法,同时属性没有创建在原型链上,因此多个子类不会共享同一个属性。
2. 子类可以动态传递参数给父类
3. 父类构造函数只执行了一次

但是还有一个问题:
子类如果在原型上添加方法,必须要在继承之后添加,否则会覆盖原来原型上的方法。但是如果这两个类是已存在的类,就不行了

优化一下:

function inheritPrototype(subType, superType){ 
    // 继承父类的原型
    var prototype = Object.create(superType.prototype);
    // 重写被污染的construct
    prototype.constructor = subType; 
    // 重写子类的原型  
    subType.prototype = Object.assign(prototype, subType.prototype); 
}

虽然通过Object.assign来进行copy解决了覆盖原型类型的方法的问题,但是Object.assign只能够拷贝可枚举的方法,而且如果子类本身就继承了一个类,这个办法也不行。

8. ES6 实现继承

我们知道了ES5中可以通过原型链来实现继承,ES6提供了extends关键字来实现继承,这相对而言更加清晰和方便,首先看看ES6 Class的语法,此处参考http://es6.ruanyifeng.com/#docs/class

8.1 Class基本语法

1.需要注意的地方。ES6 中类内部定义的所有方法都是不可枚举的
类的属性名称可以使用表达式(区别1)

2.严格模式,ES6 class类和模块内部默认是严格模式

3.construct方法
也就是类的默认方法,如果没有显示的定义,那么会添加一个空的contruct方法
返回值:默认返回实例对象,也就是this,当然也可以显式的返回另外一个对象。
例如:

Class Foo {
    constructor() {
    }
}


new Foo() instanceof Foo // true


Class FakeFoo {
    constructor() {
        return Object.create(null);
    }
}


new Foo() instanceof Foo // false

此外类必须通过new 操作符来调用,否则会报错,这个它与普通的构造函数的区别

Foo()


// TypeError: Class constructor Foo cannot be invoked without 'new'

4.类的实例对象

类的实例的属性,除非显式的定义在this上,否则都是定义在原型上,这里与ES5保持一致

5.类的表达式

与函数一样,类也可以用表达式的方式来定义

const HClass = class Me {
    getClassName() {
        return Me.name;
    }
}


const hIns = new HClass();
HClass.getClassName(); // Me
Me.getClassName(); // error

这里只有HClass是暴露在外部的,Me只有在class的内部使用,如果不需要使用Me,完全可以省略

那么我们知道利用函数表达式可以创建一个立即执行函数,类可以么?



let person = new class {
    constructor(name) {
        this.name = name;
    },
    sayName() {
        console.log(this.name);
    }
}('jack');


persion.sayName()

6.不存在变量提升
这点是和ES5不一样的, ES6并不会把class的声明提到当前作用域的顶部,这与下一节的继承有关系

new Foo()
class Foo {}

7.私有属性和私有方法

私有方法ES6并不提供,但是可以变通

  • 命名区分
  • 把方法移出模块
  • 利用Symbol来命名方法名
const getAge = Symbol('getAge');


export defalut class Person {
    // 公有方法
    getName(name) {
        return name;
    },
    // 私有方法
   [getAge](age) {
    return age;
   }
}

私有属性ES6也不支持,有提案说加个#表示私有属性

8.this的指向(仔细看看)
类的内部this的指向默认是指向this的实例的,如果单独使用类中的一些包含this的方法,很有可能会报错

class Logger {
    printName (name = 'there') {
        this.print(`Hello ${name}`);
    },
    print (text) {
        console.log(text);
    }
}


const logger = new Logger();
const {printName} = logger;
printName();
// Uncaught TypeError: Cannot read property 'print' of undefined
logger.printName()
// Hello there

解决办法:

  • 在构造函数中绑定this,这样就不会找不到print方法了
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }


  // ...
}
  • 在构造函数中使用箭头函数
  • 使用proxy代理函数,包装

9.name属性

10.class中使用get和set函数,可以用来拦截这个属性的存取行为,利用getOwnPropertyDescriptor来查看属性的get和set函数是否有定义

11.如果在类里面在某个方法上加上*,则表示这个方法是Generator函数

12.在类的某个方法前面加上static关键字,表示这个方法是静态方法,这个方法不会被实例继承,只能够通过类来调用,如果这个静态方法中有this,那么this指向的是类,而不是实例
此外静态方法,和非静态方法是可以重名滴

class Foo {
  static bar () {
    this.baz();
  }
  static baz () {
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}


Foo.bar() // hello

父类的静态方法可以被子类继承

13.类的静态属性,也就是说是通过类直接访问的属性

Class Foo {
    p = 1,

    static: 1,
}

上面的两种方法都是错误的,目前静态属性还处于提案中,

Class Foo {
    p = 1static p = 1;
}

以前我们定义实例属性只能够在construct中定义

14.new.target属性, new.target返回new命令作用的那个构造函数,如果没有通过new来实例对象,那么这个属性的值是undefined

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}


var person = new Person('Jack'); // 正确
var notAPerson = Person.call(person, 'Jack');  // 报错

在Class内部调用的时候,new.target返回当前的Class,需要注意一点就是当子类继承父类的时候,返回当前的Class

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
  }
}


class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}


var obj = new Square(3); // 输出 false

利用这个特点我们可以写出这样的代码

class Rectangle {
  constructor(length, width) {
    ifnew.Target === Rectangle) {
     throw new Error('本类不能实例化');
    }
  }
}


class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}


var obj = new Square(3);
var notobj = new Rectangle();

8.2 Class的继承

1.基本概念

Class可以通过extends关键字来实现继承,而ES5中是通过修改原型链来实现继承

子类必须在constructor中调用super方法,否则新建实例的时候会报错,因为子类没有自己的this,是继承与父类,然后进行加工。

class Point { /* ... */ }


class ColorPoint extends Point {
  constructor() {
  }
}


let cp = new ColorPoint(); // ReferenceError

我们回忆一下ES5的继承,实质是首先创建了子类的实例对象,然后把父类的方法添加到子类上。而ES6是先创建父类的实例对象,然后再用子类的构造函数修改this,如果子类没有添加constructor,这个方法会被自动添加

class ColorPoint extends Point {
}


// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

还有一点需要注意,在子类的构造函数中,只有调用super后,才可以使用this关键字,否则会报错

2.super关键字,super可以作为函数和对象来使用

  • super作为函数调用时代表父类的构造函数,这里代表A的构造函数,但是返回的是B的实例。作为函数调用时,只能在子类的构造函数调用,如果在其他地方调用会报错。
class A {}


class B extends A {
  constructor() {
    super();
    // 等价于A.prototype.constructor.call(this)
  }
}
  • super作为对象,在普通的方法中,指向父类的原型对象;在静态函数中,指向父类。
class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}


class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}


let b = new B();
b.m() 

ES6 规定,通过super调用父类的方法时,方法内部的this指向当前的子类实例

由于this指向子类的实例,当对super的一个属性复制的时候,赋值会变成子类的属性

3.ES6的proto和prototype

我们知道在ES5中,每个对象的proto属性,指向对应构造函数的prototype。而ES6里面有两条继承链路,先看一个例子

class A {
}


class B extends A {
}


B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
  • 子类的proto属性指向父类,表示构造函数的继承
  • 子类的原型的proto指向父类的原型,表示方法的继承
class A {
}


class B {
}


// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);


// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);


const b = new B();

再看下一个问题,我们知道ES6是通过extends关键字来实现继承的,那么extends后面的值可以是什么类型呢?我们根据上的两条继承链路就知道,父类应该要有prototype属性,也就是说函数都可以作为父类被继承,此外我们看3中特殊情况

  • 子类继承于Object类
class A extends Object {
}


A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
  • 不存在继承
class A {
}


A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
  • 子类继承null
class A extends null {
}


A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
  1. 原生构造函数的继承

我们知道,以前原生构造函数是无法继承的,原因是因为子类无法获得原生构造函数的内部属性。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性

function MyArray() {
  Array.apply(this, arguments);
}


MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});


var colors = new MyArray();
colors[0] = "red";
colors.length  // 0


colors.length = 0;
colors[0]  // "red"

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。因此我们可以自定义原生数据结构的子类,这些是ES5无法做到的

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}


var arr = new MyArray();
arr[0] = 12;
arr.length // 1


arr.length = 0;
arr[0] // undefined

6.Mixin的实现,也就是将多个对象合并成一个对象

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}

上面是一个比较简单的做法,我们看一个完整的实现方式

function mix(...mixins) {
  class Mix {}


  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝实例属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }


  return Mix;
}


function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== "constructor"
      && key !== "prototype"
      && key !== "name"
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}


class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}

猜你喜欢

转载自blog.csdn.net/lihangxiaoji/article/details/79753473