Open wython opened 5 years ago
js没有提供面向对象编程的语法特性,也没有类的概念。在es6中可以使用class去定义类和继承,但是实际上却没有面向对象多态的这些特性。其底层依然是基于js的原型链去实现的。
js可以基于Object创建对象(如下),但是Object本质更像是键值对的哈希对象,所以基于Object字面量和简单的方式创建对象往往无法满足我们要求。
let o = new Object(); let p = { name: 'person' };
通过函数方式也可以很好的创建对象,而且从代码复用性和封装性都比以上方式好。
function Animal(type, name) { this.type = type || 'animal'; this.name = name || 'anonymity' this.sayName = function (){ console.log(`${this.type}: ${this.name}`); } } // Animal('person'); 与普通函数无异 // new Animal('person'); 返回对象
通过new操作符,经历里一下步骤
所以说,通过new方式,我们获得的是这个新创建的对象。因此,可以很方便的创造各种实例,如下:
const person = new Animal('person'); const dog = new Animal('dog'); person.sayName(); // 输出 person: anonymity dog.sayName(); // 输出 dog: anonymity
但是其实很快就有新的问题,
person.sayName === dog.sayName // 输出false
我们发现对于某些可以复用的代码,实际上重复执行了,说白了就是,sayName函数实际上只需要存在一份即可,但是实际运行过程中,不同实例都声明了不同的函数。这实际不是一个优雅的方式。所以引出下一个概念,原型链。
希望读者先区分构造函数和实例两个不同的名词,首先,如果细心就会发现,对于元生对象如Object, Array之类的(本质依然是function)构造函数,都具有prototype属性。像我们自己定义的Animal函数,也具有一个prototype属性。 这个prototype属性指向的是原型对象的引用。每一个函数在创建过程中,会生成一个原型对象,同时,构造函数本身的prototype属性指向这个原型对象。细心点也会发现,通过new创建的实例person,dog具有proto属性。这是一个已废弃的属性,但是它的存在说明了实例上面也有指向原型对象的引用。不推荐用proto访问原型对象,那有什么办法可以访问实例的原型对象。可以通过Object.getPrototypeOf(person)方式获得实例的原型对象,实例的原型对象和构造函数的原型对象是不是一致的,答案是肯定的。
Object.getPrototypeOf(person) === Animal.prototype // true
同时,原型对象上有constructor属性是指向构造函数本身的。
Animal.prototype.constructor === Animal // true
所以可以总结为,当函数被创建时,会有一个prototype属性指向一个生成的原型对象, 并且原型对象具有指回构造函数的引用contructor属性。当通过构造函数创建实例时,实例也拥有指向原型对象的属性(即实例可以追溯原型对象)。
在别的语言里可以通过static声明公共的属性,js的原型对象有这样用处。因为实例访问属性的过程中,会遍历自身属性,如果没有会追溯原型链的属性。也就是说,实例可以访问到原型对象上的属性。同时所有实例共享同一个原型对象。
Object.getPrototypeOf(person) === Object.getPrototypeOf(dog); // true
这也就达到一份代码可以多实例复用的要求。所以构造函数写成:
function Animal(type, name) { this.type = type || 'animal'; this.name = name || 'anonymity' } Animal.prototype.sayName = function (){ console.log(`${this.type}: ${this.name}`); } Animal.prototype.publicProper = 'p';
如果希望构造函数看上去封装性更好,可以优化
function Animal(type, name) { this.type = type || 'animal'; this.name = name || 'anonymity' if (typeof this.sayName !== 'function') { Animal.prototype.sayName = function (){ console.log(`${this.type}: ${this.name}`); } } }
讲了这么久,才到真正要讨论的内容,因为js真正属于面向对象特性的还是继承。有以上基础其实可以很简单的实现继承。最简单,最直接一般都是这样做
function Animal(type, name) { this.type = type || 'animal'; this.name = name || 'anonymity' if (typeof this.sayName !== 'function') { Animal.prototype.sayName = function () { console.log(`${this.type}: ${this.name}`); } } } function Person(type, name) { this.name = name | 'anonymity'; } Person.prototype = new Animal('person');
这样最简单的继承就实现了,但是这种继承方式存在缺点。
因为子类Person的原型是Animal的实例,所Person创建的实例也包含了Animal实例的属性,Person不同实例共用Animal的实例属性。是不是有办法,当Animal的实例属性继承下来是Person的实例属性。
Animal构造函数是有参数的,这种方式因为是公用的,所以我们继承new Animal时候没有传参数name,因为我们默认Animal的name属性是需要覆盖的。明显这样写法灵活性不够。或者说,如果希望type, name属性依然是实例属性,实际上继承Animal构造函数的代码没有复用到。
子类新原型对象并没有constructor属性指回构造函数,与原生方式不同。
可以通过在子类中调用父类构造函数,并且改变作用域,将实例属性赋值到子类的实例属性上
function Animal(type, name) { this.type = type || 'animal'; this.name = name || 'anonymity' if (typeof this.sayName !== 'function') { Animal.prototype.sayName = function () { console.log(`${this.type}: ${this.name}`); } } } function Person(name) { this.job = null; Animal.call(this, 'person', name); } Person.prototype = new Animal(); Person.prototype.constructor = Person; const p = new Person('peter'); p.sayName(); // Person peter
这样实现,大体上是已经很完美了, 但是依然有不足,一个是构造函数执行两次,所以原型属性上和实例属性上重复了。为了解决这个问题,我们最直观的想法时就是创建一个空对象并且,然后将原型上的内容复制到空对象上。实际上,我们一个可以创建一个空白构造函数,并且使其原型为父类原型,再将空白对象实例作为之类原型即可。用代码直观点:
function objectCreate(proto) { function F(){}; F.prototype = proto; return new F(); } // 继承代码改写为 function Animal(type, name) { this.type = type || 'animal'; this.name = name || 'anonymity' if (typeof this.sayName !== 'function') { Animal.prototype.sayName = function () { console.log(`${this.type}: ${this.name}`); } } } function Person(name) { this.job = null; Animal.call(this, 'person', name); } Person.prototype = objectCreate(Animal.prototype); Person.prototype.constructor = Person; const p = new Person('peter'); p.sayName(); // Person peter
当然es5中已经提供了object.create()方法。
到此,继承的所有讨论就结束了,在es6,和ts已经这么普及的今天,讨论这一个老生常谈的问题,有必要吗?我觉得还是有必要的,第一,我觉得对于这样问题,并不是所有人都真正的理解其中的含义。第二,我觉得只要浏览器没有完全支持新语法,那么意味着我们最终跑得前端代码依然是很原始的代码,在这个角度去理解这些代码是有益的。最后,我个人也觉得,对于继承这个话题,在红皮书上有各种各样的定义,很多人太拘泥于它的命名,反而忽视了一些内容背后解决的问题,这些命名其实是从英文在通过翻译者翻译,并不是真正需要去理解的东西。
谈js继承
js没有提供面向对象编程的语法特性,也没有类的概念。在es6中可以使用class去定义类和继承,但是实际上却没有面向对象多态的这些特性。其底层依然是基于js的原型链去实现的。
js可以基于Object创建对象(如下),但是Object本质更像是键值对的哈希对象,所以基于Object字面量和简单的方式创建对象往往无法满足我们要求。
通过函数方式创建对象
通过函数方式也可以很好的创建对象,而且从代码复用性和封装性都比以上方式好。
通过new操作符,经历里一下步骤
所以说,通过new方式,我们获得的是这个新创建的对象。因此,可以很方便的创造各种实例,如下:
但是其实很快就有新的问题,
我们发现对于某些可以复用的代码,实际上重复执行了,说白了就是,sayName函数实际上只需要存在一份即可,但是实际运行过程中,不同实例都声明了不同的函数。这实际不是一个优雅的方式。所以引出下一个概念,原型链。
原型对象
希望读者先区分构造函数和实例两个不同的名词,首先,如果细心就会发现,对于元生对象如Object, Array之类的(本质依然是function)构造函数,都具有prototype属性。像我们自己定义的Animal函数,也具有一个prototype属性。 这个prototype属性指向的是原型对象的引用。每一个函数在创建过程中,会生成一个原型对象,同时,构造函数本身的prototype属性指向这个原型对象。细心点也会发现,通过new创建的实例person,dog具有proto属性。这是一个已废弃的属性,但是它的存在说明了实例上面也有指向原型对象的引用。不推荐用proto访问原型对象,那有什么办法可以访问实例的原型对象。可以通过Object.getPrototypeOf(person)方式获得实例的原型对象,实例的原型对象和构造函数的原型对象是不是一致的,答案是肯定的。
同时,原型对象上有constructor属性是指向构造函数本身的。
所以可以总结为,当函数被创建时,会有一个prototype属性指向一个生成的原型对象, 并且原型对象具有指回构造函数的引用contructor属性。当通过构造函数创建实例时,实例也拥有指向原型对象的属性(即实例可以追溯原型对象)。
原型对象作用之共用公共属性
在别的语言里可以通过static声明公共的属性,js的原型对象有这样用处。因为实例访问属性的过程中,会遍历自身属性,如果没有会追溯原型链的属性。也就是说,实例可以访问到原型对象上的属性。同时所有实例共享同一个原型对象。
这也就达到一份代码可以多实例复用的要求。所以构造函数写成:
如果希望构造函数看上去封装性更好,可以优化
原型对象作用二之继承
讲了这么久,才到真正要讨论的内容,因为js真正属于面向对象特性的还是继承。有以上基础其实可以很简单的实现继承。最简单,最直接一般都是这样做
这样最简单的继承就实现了,但是这种继承方式存在缺点。
因为子类Person的原型是Animal的实例,所Person创建的实例也包含了Animal实例的属性,Person不同实例共用Animal的实例属性。是不是有办法,当Animal的实例属性继承下来是Person的实例属性。
Animal构造函数是有参数的,这种方式因为是公用的,所以我们继承new Animal时候没有传参数name,因为我们默认Animal的name属性是需要覆盖的。明显这样写法灵活性不够。或者说,如果希望type, name属性依然是实例属性,实际上继承Animal构造函数的代码没有复用到。
子类新原型对象并没有constructor属性指回构造函数,与原生方式不同。
借用父类构造函数
可以通过在子类中调用父类构造函数,并且改变作用域,将实例属性赋值到子类的实例属性上
这样实现,大体上是已经很完美了, 但是依然有不足,一个是构造函数执行两次,所以原型属性上和实例属性上重复了。为了解决这个问题,我们最直观的想法时就是创建一个空对象并且,然后将原型上的内容复制到空对象上。实际上,我们一个可以创建一个空白构造函数,并且使其原型为父类原型,再将空白对象实例作为之类原型即可。用代码直观点:
当然es5中已经提供了object.create()方法。
到此,继承的所有讨论就结束了,在es6,和ts已经这么普及的今天,讨论这一个老生常谈的问题,有必要吗?我觉得还是有必要的,第一,我觉得对于这样问题,并不是所有人都真正的理解其中的含义。第二,我觉得只要浏览器没有完全支持新语法,那么意味着我们最终跑得前端代码依然是很原始的代码,在这个角度去理解这些代码是有益的。最后,我个人也觉得,对于继承这个话题,在红皮书上有各种各样的定义,很多人太拘泥于它的命名,反而忽视了一些内容背后解决的问题,这些命名其实是从英文在通过翻译者翻译,并不是真正需要去理解的东西。