深入理解ES5,ES6创建对象和继承

1,180 阅读10分钟

ES5创建对象

构造函数

作用:创建特定类型的对象

function Animal(species) {  //构造函数借鉴其他OO语言,规定惯例,构造函数始终以大写字母开头命名 
   this.species = species; 
   this.saySpecies = function(){ 
       console.log(this.species); 
   }; 
}

任何函数,只要通过new操作符来调用,那它就可以作为构造函数。那如果不使用new操作符来调用Person()呢?

Animal("兔"); 
//属性和和方法都被添加给Global对象了,如果此时是在浏览器中,就是window对象 
window.saySpecies(); // "兔"

构造函数的主要问题:每个方法要在每个实例上重新创建一遍,他们不是同一个Function的实例,创建两个完成同样任务的Function实例没必要

alert(animal1.saySpecies == animal2.saySpecies); // false 
//为什么说他们是Function的实例?下面这种写法方便理解,但不推荐 
function Animal(species) {          
    this.species = species;          
    this.saySpecies = new Function("alert(this.species)"); 
}

解决这个问题的两个方案:全局函数(不推荐)和原型模式

function Animal(species) { 
    this.species = species; 
    this.saySpecies = saySpecies;
    //第二个sayName函数名包含的是一个指向函数的指针,所以animal1和animal2中的this.saySpecies调用的是同一块内存地址里的saySpecies()函数 
} 
function saySpecies(){ 
    console.log(this.species);
}

原型模式

构造函数、原型对象、实例对象三者间的关系: 无论在什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。所有的原型对象会自动获得一个constructor(构造函数)属性,这个属性包含一个指针,指向prototype属性所在函数(即构造函数)。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象。ECMA-261第5版管这个指针叫[[prototype]],一些浏览器厂商支持一个叫__proto__的属性。注意:这个指针连接的是实例原型对象,而不是实例和构造函数。

放上一张经典老图: 屏幕快照 2019-08-29 下午12.06.01.png 当为对象实例添加一个属性时,这个属性就有屏蔽原型对象中的同名属性。即添加这个属性只会组织我们访问原型中的那个属性,但不会修改那个属性。

function Animal() { 
    //构造函数被掏空	  
} 
Animal.prototype.species = "某种动物"; 
Animal.prototype.areas = ["中国", "美国"]; Animal.prototype.saySpecies = function(){     
    console.log(this.species); 
};     
var animal1 = new Animal(); //省略了为构造函数传递初始化参数了,所以所有实例在默认情况下取得相同属性值 
var animal2 = new Animal();

//animal1和animal2共享了Animal原型对象的属性和方法,但animal1想有自己的专属种类 
animal1.species = "狗"; 
console.log(animal1.species);// “狗”——来自实例 
console.log(animal2.species); // “某种动物”——来自原型   
// 可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值,但如果是重写包含引用类型值的属性值呢?
animal1.areas.push("日本"); 
console.log(animal2.areas); // [ '中国', '美国', '日本' ]  
//我本来只想改下animal1的地区的,但结果是吧animal2也改了
//所以通常组合使用函数模式和原型模式,构造函数模式用来定义实例属性,原型模式用来定义方法和共享的属性

ES5继承机制

js的设计者最初只是想设计一种简易的脚本语言,让浏览器可以和网页互动,所以不需要有”继承“机制,但需要有一种机制把所有的对象联系起来。所以最后还是设计了”继承“,但是没有引入类的概念。

使用原型链:

原理:让原型对象等于另一个类型的实例

function Animal(species){ 
    this.species = species; 
    this.areas = ["中国", "美国"]; 
} 
Animal.prototype.saySpecies = function(){ 
    console.log(this.species); 
}; 
//--------------------------------------- 
function Dog(name){
    this.name = name; 
} 
Dog.prototype = new Animal("狗");// 此处把Animal实例赋给Dog.prototype 
Dog.prototype.sayName = function(){ 
    console.log(this.name); 
}; 
//--------------------------------------- 
var myDog1 = new Dog("旺财"); 
myDog1.saySpecies; // 狗 成功继承了Animal类型的saySpecies方法 
//--------------------------------------- 
var myDog2 = new Dog("旺旺");
myDog2.areas.push("日本"); 
console.log(myDog1.areas);//[ '中国', '美国', '日本' ] 
//引用类型值的问题又来了,我改的是Dog2的地区,把Dog1的地区也改了

原型链的问题:

  • 之前说过,如果原型属性包含了引用类型值,那么它会被所有的实例共享,所以后来现在我们把一些不想被共享的属性放在了构造函数里。在通过原型链来实现继承的时候,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章变成了现在的原型属性了。
  • 创建子类型的实例时,不能向超类型的构造函数中传递参数

原型链示例图:要注意现在myDog.prototype.constructor指向Animal,另外别忘了Object,所有的引用类型默认都继承了Object

组合继承

使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。

function Animal(species){ 
    this.species = species; 
    this.areas = ["中国", "美国"]; 
} 
Animal.prototype.saySpecies = function(){ 
    console.log(this.species); 
}; 
//--------------------------------------- 
function Dog(name, species){ 
    this.name = name; 
    Animal.call(this, species); // 新增了这么一句,去掉了Dog.prototype = new Animal("狗"); 
} 
// 借用构造函数继承:使用call或者apply方法,在子类型构造函数的内部调用超类型构造函数 
Dog.prototype = new Animal(); 
Dog.prototype.constructor = Dog; 
// 因为Dog.prototype = new Animal();这句话让Dog.prototype.constructor指向Animal了,更重要的是每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性 
// 如果dog1.constructor也指向Animal,这就很奇怪了,明明dog1是用构造函数Dog生成的,所以我们要手动把它扳回来,让Dog.prototype对象的constructor值改为Dog 
Dog.prototype.sayName = function(){ 
    console.log(this.name);
}; 
//--------------------------------------- 
var myDog1 = new Dog("旺财", "狗"); 
var myDog2 = new Dog("旺旺"); 
myDog2.areas.push("日本");
console.log(myDog1.areas); //[ '中国', '美国'] 
// 这次没有影响到myDog1了 
console.log(myDog1.saySpecies == myDog2.saySpecies) // true

这样既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。

ES6创建类

引入了Class(类)这个概念,新的class写法让对象原型的写法更加清晰,将抽象思维具体化。更加像面向对象的语言。其实它的绝大部分功能ES5也可以做到。

// 创建Animal类 
class Animal { 
    constructor(species){ // 构造函数没了,变成了constructor构造方法 
    this.species = species; // species是实例对象animal1自身的属性(因为定义在this变量上) 
    } 
    saySpecies(){ // 方法前面的function关键字没了 
        console.log(this.species); 
    } 
} 
// 实例化Animal类 let animal1 = new Animal("狗"); animal1.saySpecies(); // ”狗“

相似点

  • 实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。在上面的代码中species是实例对象animal1自身的属性(因为定义在this变量上),而saySpecies方法是原型对象的方法(因为定义在Animal类上)。
  • 类可以看成是构造函数的另一种写法,构造函数的prototype属性,在 ES6 的“类”上面继续存在。而且类的数据类型就是函数(typeof Point // "function"),类本身就指向构造函数。事实上,类的所有方法都定义在类的prototype属性上面。在类的实例上调用方法其实就是调用原型上的方法,比如说下面的例子:animal1实例的constructor方法,其实就是Animal类原型的constuctor方法。
    console.log(animal1.constructor === Animal.prototype.constructor) // true
    
  • prototype对象的 constructor 属性,直接指向“类”的本身,这与 ES5 的行为是一致的。
    console.log(Point.prototype.constructor === Point) // true
    

不同点:

  • 类必须使用new调用,否则会报错,这和前面提到的es5的构造函数有所不同

屏幕快照 2019-08-29 下午5.31.03.png 屏幕快照 2019-08-29 下午5.28.12.png

  • 类不存在变量提升,这一点也和ES5不同,如果类的使用在前,定义在后,就会报错;这么做的原因与类的继承有关,因为要保证子类在父类之后定义。
new Animal(); // ReferenceError class Animal {}
  • this的指向: 类的方法内部如果含有 this ,它默认指向类的实例。注意以下情况:把方法提取出来单独用很危险
class Logger { 
    printName(name = 'there') { 
        this.print(`Hello ${name}`); 
    } 
    print(text) { 
        console.log(text); 
    } 
} 
const logger = new Logger(); 
const { printName } = logger; //printName方法被单独拎出来了 
printName(); // TypeError: Cannot read property 'print' of undefined 
// 因为此时printName方法中的this指向方法运行时的环境,所以找不到print方法,就报错了

常用的解决办法:在构造方法里绑定this。无论printName方法被提取到哪里使用,它的this指向的永远是当前实例化对象。用这个更好: www.npmjs.com/package/aut…

constructor() { 	
    this.printName = this.printName.bind(this); 
}

ES6类的继承

class Animal { 	
    constructor(species) {  		
        this.species = species; 
    } 	
    saySpecies() {     	
        console.log(this.species); 	
    } 
} //定义一个Dog类继承Animal类
class Dog extends Animal {  	
    constructor(species, name) { 
        super(species); // 调用父类的constructor(species),super用来新建父类的this对象,子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super方法,子类就得不到 this 对象。
        this.name = name;  	
     } 	
     sayNameAndSpecies() { 		
         console.log(this.name, this.species); 	
     } 
}  
let dog1 = new Dog("狗","旺财"); 
dog1.sayNameAndSpecies();

从形式上来看:ES5先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this) )。

ES6 先创造父类的实例对象 this (所以必须先调用 super 方法),然后再用子类的构造函数修改 this 。

类的prototype属性和__proto__属性:

在ES5中,每个实例对象都有__proto__属性,指向原型对象,构造函数的prototype属性也指向原型对象。而class同时具有prototype属性和 proto 属性.

ES6中有两条继承链:

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

可以这样理解:作为一个对象,子类( B )的原型( proto 属性)是父类( A );作为一个构造函数,子类( B )的原型对象 ( prototype 属性)是父类的原型对象( prototype 属性)的实例。

ES6类的继承实现原理

es6类的继承用babel转换后(重点是_inherits函数)

"use strict";  
function _typeof(obj) {  	
    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
        _typeof = function _typeof(obj) { 
        return typeof obj; 		
    };  	
    } else { 		
        _typeof = function _typeof(obj) {  			
            return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 
        }; 	
    }  	
    return _typeof(obj);  
} 
//---------------------------------------------------------------------------- 
function _possibleConstructorReturn(self, call) {  //生成并返回一个调用父类的构造函数的this,再在主函数中用子类的构造函数进行加工 	
    if (call && (_typeof(call) === "object" || typeof call === "function")) { 
    // 如果父类返回的是对象或函数,则返回父类的构造函数生成的this,否则返回self 		
    return call;  	
    }  	
    return _assertThisInitialized(self);  
}  
function _assertThisInitialized(self) { 	
    if (self === void 0) {  		
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 
    }  	
    return self; 
} 
//----------------------------------------------------------------------------------- 
function _getPrototypeOf(o) { //返回子类o的父类 	
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o);
}; 				    
return _getPrototypeOf(o); 
} 
//------------------------------------------------------------------------------------- 
function _inherits(subClass, superClass) {  // 用于实现继承的函数 
    if (typeof superClass !== "function" && superClass !== null) {  
        throw new TypeError("Super expression must either be null or a function");  	
     } 	
     subClass.prototype = Object.create(superClass && superClass.prototype, { // 让子类的原型对象继承父类的原型对象 
         // 给子类添加 constructor属性 
         subclass.prototype.constructor === subclass
         constructor: {  			
             value: subClass,  			
             writable: true,  			
             configurable: true,	
         }	
    }); 	
    if (superClass) _setPrototypeOf(subClass, superClass); // 子类__proto__ 指向父类 
}  
function _setPrototypeOf(o, p) {  	
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; 
    return _setPrototypeOf(o, p); 
}
//------------------------------------------------------------------------------------  
function _instanceof(left, right) { 
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
    return !!right[Symbol.hasInstance](left);  	
    } else { 		
        return left instanceof right;  
    }  
}  
function _classCallCheck(instance, Constructor) { // 防止以函数的方式调用class 	
    if (!_instanceof(instance, Constructor)) {  		
        throw new TypeError("Cannot call a class as a function");
    }  
} 
//------------------------------------------------------------------------------------ 
function _defineProperties(target, props) {  	
    for (var i = 0; i < props.length; i++) {  		
        var descriptor = props[i];  		
        descriptor.enumerable = descriptor.enumerable || false;  //默认类的所有方法都是不允许枚举的 		
        descriptor.configurable = true;  //表示能够通过delete删除属性从而重新定义 		
        if ("value" in descriptor) descriptor.writable = true; //默认方法可修改 		
        Object.defineProperty(target, descriptor.key, descriptor);  //在目标对象target上定义一个新属性 	
    }  
}  
function _createClass(Constructor, protoProps, staticProps) { 
    // 利用_defineProperties给类添加方法 	
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);  //把非静态方法添加到构造函数的原型对象上 	
    if (staticProps) _defineProperties(Constructor, staticProps); //把静态方法添加到构造函数上 	
    return Constructor;  
}  
var Animal = 
/*#__PURE__*/ 
function () {   
    function Animal(species) {    
        _classCallCheck(this, Animal);      
        this.species = species; //在构造函数里绑定实例的属性   
    }    
    _createClass(Animal, [{  
        //传递两个参数,类、绑定在类的prototype上的方法,如果有静态方法的话也要传过去     key: "saySpecies",    
        value: function saySpecies() {       
            console.log(this.species);     
        }   
    }]);    
    return Animal; 
}();  
var Dog = 
/*#__PURE__*/ 
function (_Animal) {  
    _inherits(Dog, _Animal); //继承父类    
    function Dog(species, name) {     
        var _this;      
        _classCallCheck(this, Dog);     
        _this = _possibleConstructorReturn(this, _getPrototypeOf(Dog).call(this, species)); 	
        //把super()那句变成了这个,_possibleConstructorReturn第一个参数是指向子类实例的this,另一个参数是调用父类的构造函数返回的父类实例 	//_getPrototypeOf(Dog)函数 	
        //_getPrototypeOf(Dog).call(this, species)对比es5借用构造函数继承这句Animal.call(this, species); 	
        // 都是在子类型构造函数内部调用父类型构造函数,有哪里不同? 
        _this.name = name;     
        return _this;
     }    
    _createClass(Dog, [{    
        key: "sayNameAndSpecies",     
        value: function sayNameAndSpecies() {  
            console.log(this.species, this.name);
         }   
     }]);    
     return Dog; 
}(Animal);  
var dog1 = new Dog("狗", "旺财"); 
dog1.sayNameAndSpecies();

Object.create(proto[, propertiesObject])方法

Object.create方法用法:
![]( "李哲 > es5 es6类和实例化学习备忘 > 屏幕快照 2019-08-30 上午8.25.45.png")

用法:

Dog.prototype = Object.create(Animal.prototype); 
Dog.prototype.constructor = Dog;

什么情况用类,什么情况不用

参考链接:你可以不会 class,但是一定要学会 prototype

使用类可以带来的好处:使用类的话,在发生内存泄露时,查看memory里面的profiles,通过Class filter能够更加快速方便的找到它,可以很清晰的看到在哪里调用了它