浅谈JavaScript面向对象

前言

对象(Object)应该算是js中最为重要的部分,也是js中很是难懂晦涩的一部分。更是面试以及框架设计中各出没。写这篇文章,主要参考与JavaScript红宝书(JavaScript高级程序设计 第六章章节)以及各大博主博客。
原文地址:https://github.com/Nealyang/YOU-SHOULD-KNOW-JSjavascript

谈谈对象属性的特性

毕竟是面向对象编程,咱们在讨论如何面向对象以前先讨论讨论对象具备哪些属性和特性。html

属性类型

简单的说,对象拥有四个属性:前端

  • [[Configurable]]:是否能够经过delete删除,可否修改属性的特性。直白点:是否可配置
  • [[Enumerable]]:枚举性,表示是否能够经过for-in循环返回
  • [[Writable]]:可写性:是否能够修改属性的值
  • [[Value]]:包含属性的值,也就是对应的可读性。
    以上四个对象的属性的属性类型默认值分别为:true,true,true,undefined。

若是要修改属性默认的特性,必须经过Object.defineProperty()方法。大体以下:java

var animal = {};
Object.defineProperty(animal,"name",{
    writable:false,
    value: 'dog';
});
console.log(animal.name);//dog
animal.name = 'cat';
console.log(animal.name);//dog复制代码

从上面的实例你们也能看出,在调用Object.defineProperty()方法后,若是不指定 configurable、enumerable、writable 特性的值时,默认为FALSE。git

访问器属性

访问器属性不包含数据值,可是包含getter和setter函数。在读取访问器属性时,会调用getter函数,这个函数负责返回有效值。在写入访问器属性时,回到用setter函数并传入新值。github

  • [[Configurable]]:表示是否能够经过delete删除。默认为TRUE
  • [[Enumerable]]:同上面介绍的Enumerable同样,默认为true
  • [[Get]]:读取数据时候调用的方法。默认为undefined
  • [[Set]]:在写入属性值得时候默认调用的方法。默认为undefined

这里不作过多解释,直接看例子吧(来自js红宝书)web

var book = {
    _year:2012,
    edition:1
};
Object.defineProperty(book, 'year',{
    get:function(){
        return this._year
    },
    set:function(value){
        if(value>2012){
            this._year = value,
            this.edition++
        }
    }
});

book.year = 2013;
console.log(book.edition);//2复制代码

其实对于多个属性的定义,咱们可使用Object.defineProperties方法。而后对于读取属性的特性咱们可使用Object.getOwnPropertyDescriptor()方法。你们自行查看哈。面试

建立对象

建立对象,咱们不是直接能够经过Object的构造函数或者对象字面量的方法来实现对象的建立嘛?固然,这些方法是能够的,可是有一个明显的缺点:使用同一个接口建立不少对象,产生大量重复的代码。因此这里,咱们使用以下的一些骚操做编程

工厂模式

一种很基础的设计模式,简而言之就是用函数来封装以特定接口建立对象的细节。设计模式

function createAnimal(name,type){
    var o = new Object();
    o.name = name;
    o.type = type;
    o.sayName = function(){
        alert(this.name)
    }
    return o;
}
var cat = createAnimal('小猫','cat');
var dog = createAnimal('小🐽','dog');复制代码

优势:能够无数次的调用这个函数,来建立类似对象。
缺点:不能解决对象识别的问题。也就是说,我不知道你是谁家的b孩子

构造函数模式

ECMAScript中的构造函数能够用来建立特定类型的对象。在运行时会自动出如今执行环境中(这句话后面讲解this的时候仍是会说到)。

function Animal(name,type){
    this.name = name;
    this.type = type;
    this.say = function(){
        alert(this.name);
    }
}

var cat = new Animal('小猫','cat');
var dog = new Animal('小🐽','dog');复制代码

注意上面咱们没有显示的return过一个对象出来,为何?由于this(后面会讲this的)。

关于构造函数惯例首字母大写就不啰嗦了。强调构造函数必定要使用关键字new来调用。为何使用new呢?由于你使用了new,他会

  • 建立一个新的对象
  • 将构造函数的做用域赋值给新对象(this执行新的对象)
  • 执行构造函数的代码
  • 返回新的对象

那么解决了工厂模式的诟病了么?固然~

在实例对象中,都有一个constructor属性。

cat.constructor == Animal //true
dog.constructor == Animal //true
cat instanceof Animal //true
dog instanceof Animal //true复制代码

构造函数模式的优势如上所说,可是缺点仍是有的,好比说

cat.sayName == dog.sayName //false复制代码

也就是说,他建立了两个功能同样的函数,这样是很没有必要的,固然,咱们能够把sayName放到构造函数外面,而后经过this.sayName=sayName来操做,可是这样的话,又会致使全局变量的污染。肿么办???

原型模式

咱们在建立每个函数的时候都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。而这个对象的用途就是包含由特定类型的全部实例共享的属性和方法。

function Animal() {}
Animal.prototype.name = '毛毛';
Animal.prototype.type = 'dog';
Animal.prototype.sayName = function() {
  alert(this.name);
}
var cat = new Animal();
var dog = new Animal();
alert(cat.sayName == dog.sayName)//true复制代码

原型模式的好处就是可让全部的对象实例共享他的属性和方法。没必要在构造函数中定义对象实例的信息。

function Person() {}
Person.prototype.name = 'Nealyang';
Person.prototype.age = 24;
Person.prototype.sayName = function(){
  alert(this.name);
}
var neal = new Person();
console.log(neal.name)//'Nealyang' -> 来自原型
neal.name = 'Neal';
console.log(neal.name)// Neal -> 来自实例

delete neal.name;
console.log(neal.name)//'Nealyang' -> 来自原型复制代码

上面的例子说明两点

  • 原型中的对象属性能够被实例所覆盖重写
  • 经过delete能够删除实例中的属性,可是删除不了对象上的

咱们能够经过hasOwnProperty()方法来肯定一个属性是在原型上仍是在实例上。person1.hasOwnProperty('name'),若是name为实例属性,则返回true。
咱们也能够经过 'name' in person1 来肯定,person1上是否有name这个属性。

上面你们可能已将发现,这种原型模式的写法很是的繁琐,有了大量的XXX.prototype. 这里有一种简写的形式。
参照具体说明参照阮神的博客 面向对象第二篇

function Person(){}
Person.prototype = {
    constructor:Person,
    name:"Neal",
    age:24,
    job:'Software Engineer',
    sayName:function(){
        alert(this.name);
    }
}复制代码

上面代码特地添加了一个constructor属性,由于每建立一个函数,就会自动建立他的prototype对象,这个对象会自动获取contractor属性。而咱们这中写法,本质上重写了默认的prototype对象,所以,constructor属性也就变成新的对象的constructor属性了(指向Object构造函数),因此这里的简写方式,必定要加上constructor。

下面咱们再谈一谈原型模式的优缺点。

优势,正如上面咱们说到的,能够省略为构造函数传递出实话参数这个环节,而且不少实例能够共享属性和方法。正是由于原型中全部的属性是被全部的实例所共享的,这个特性在方法中很是实用,可是对于包含引用类型的属性来讲问题就比较突出了。

function Person(){};

Person.prototype = {
    constructor:Person,
    name:"neal",
    friends:['xiaohong','xiaoming'],
    sayName:function(){
        alert(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push('xiaohua');

alert(person1.friends);//'xiaohong','xiaoming','xiaohua'
alert(person2.friends);//'xiaohong','xiaoming','xiaohua'
alert(person1.friends == person2.friends)//true复制代码

因为friends数组存在于Person.prototype上,并非person1上面,因此当咱们修改的时候,其实修改的是全部实例所共享的那个值。

组合使用构造函数和原型模式

这是建立自定义类型最多见的一种方式。就是组合使用构造函数和原型模式.构造函数模式用于定义实力属性,原型模式用于定义方法和共享的属性。

function Person(name,age){
    this.name = name,
    this.age = age
}

Person.prototype = {
    constructor:Person,
    sayName:function(){
        alert(this.name);
    }
}

var person1 = new Person('Neal',24);
var person2 = new Person('Yang',23);
...复制代码

上面的例子中,实例全部的属性都是在构造函数中定义,而实例全部共享的属性和方法都是在原型中定义。这种构造函数和原型模式混合的模式,是目前ECMAScript中使用最为普遍的一种方法。

固然,有些人会以为独立的构造函数和原型很是的难受,因此也有推出所谓的动态原型构造模式的这么一说。

function Person(name,age){
    this.name = name,
    this.age = age,
    if(typeof this.sayName != 'function'){
        Person.prototype.sayName = function(){
            console.log(this.name)
        }
    }
}
...复制代码

注意上面的代码,以后在sayName不存在的时候,才会在原型上给他添加相应的方法。由于对原型的修改,可以当即在全部的实例中获得反应。因此这中作法确实也是很是的完美。

关于javaScript高程中说到的别的寄生构造函数模式和稳妥构造函数模式你们能够自行查看哈~这里就不作过多介绍了。

继承

说到面向对象,固然得说到继承。说到继承固然得说到原型。说到原型,这里咱们摘自网上一篇博客里的段落

为了说明javascript是一门面向对象的语言,首先有必要从面相对象的概念入手一、一切事物皆对象。二、对象具备封装和继承特性。三、对象与对象之间使用消息通讯,各自存在信息隐秘 。
javascript语言是经过一种叫作原型(prototype) 的方式来实现面向对象编程的。固然,还有好比java就是基于类来实现面向对象编程的。

基于类的面向对象和基于原型的面向对象方式比价

对于基于类的面向对象的方式中,对象依靠class类来产生。而在基于原型的面向对象方式中,对象则是依靠构造器(constructor)利用原型(prototype)构造出来的。举个客观世界的例子来讲,例如工厂造一辆汽车一方面,工人必须参照一张工程图纸,设计规定这辆车如何制造,这里的工程图纸就比如语言中的类class。而车就是按照这个类制造出来的。另外一方面,工人和机器至关于contractor,利用各类零部件(prototype)将汽车造出来。

固然,对于上面的例子两种思惟各类说法。固然,笔者更加倾向于基于原型的面向对象编程,毕竟我是前端出生(咳咳,真相了),正当理由以下:

首先,客观世界中的对象的产生都是其余实物对象构造的世界,而抽象的图纸是不能产生出汽车的。也就是说,类,是一个抽象概念的而非实体,而对象的产生是一个实体的产生。其次,按照一切事物皆对象的这饿极本的面向对象的法则来讲,类自己并非一个对象,然而原型方式的构造函数和原型自己也是个对象。再次,在类的面向对象语言中,对象的状态又对象的实例所持有,对象的行为方法则由申明该对象的类所持有,而且只有对象的构造和方法可以被继承。而在原型的面向对象语言中,对象的行为、状态都属于对象自己,而且可以一块儿被继承。

原型链

ECMAScript描述了原型链的概念,并将原型链做为实现继承的主要方法。基本思想就是利用原型让一个引用类型继承另外一个引用类型的属性和方法。

实现原型链有一种基本模式:

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType (){
    this.subproperty = false;
}

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}

var instance = new SubType();

alert(instance.getSuperValue());复制代码

在上面的代码中,咱们没有使用SubType默认提供的原型,而是给它换了一个新的原型,这个新原型就是SuperType的实例。因而,新原型不只具备所谓一个SuperType的实例所拥有的所有属性和方法,并且其内部还有一个指针,指向SuperType的原型。最终结果是这样的:instance指向subtype的原型,subtype的原型又指向SuperType的原型。

经过实现原型链,本质上是扩展了原型搜索机制。

虽然如上,咱们已经实现了javascript中的继承。可是依旧存在一些问题:最主要的问题来自包含引用类型的原型。第二个问题就是在建立子类型的实例时,不能向超类型的构造函数中传递参数。这两个问题上面也都有说到,这里就不作过多介绍,直接看解决办法!

借用构造函数

在解决原型中包含引用类型的数据时,咱们能够在子类型构造函数内部调用超类型的构造函数。直接看代码:

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

function SubType(name){
    //继承了Super
    SuperType.call(this,name)
}

var instance1 = new SubType('Neal');
alert(instance1.name)
instance1.colors.push('black');
alert(instance1.colors);//'red','yellow','black'

var instance2 = new SubType('yang');
alert(instance2.colors);//'red','yellow'复制代码

毕竟函数只不过是在特定环境中执行代码的对象,所以能够经过call活着apply方法在新建立的对象上执行构造函数。并且如上代码也解决了子类构造函数中向超类构造函数传递参数的问题

可是,这样问题就来了,相似咱们以前讨论建立的对象那种构造函数的问题:若是都是使用构造函数,那么,也就避免不了方法都在构造函数中定义,而后就会产生大量重复的代码了。

组合继承

由于考虑到上述的缺点,因此这里又使用了组合继承的方式,历史老是惊人的类似。直接看代码:

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

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

function SubType(name,age){
    //继承属性
    SuperType.call(this,name);

    this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
}

var instance1 = new SubType('Nealyang',24);
instance1.colors.push('white');
instance1.sayName();//Nealyang
instance1.sayAge();// 24

var instance2 = new SubType('Neal',21);
alert(instance2.colors);//'red','yellow'
instance2.sayName();//Neal
instance2.sayAge();//21复制代码

在上面的例子中,SuperType构造函数定义了两个属性,name和colors,SuperType的原型中定义了一个方法sayName,subtype的构造函数中调用SuperType构造函数而且传入name,而后将SuperType的实例赋值给subtype的原型。而后又在新的原型中定义了sayAge的方法。这样一来,就可让两个不一样的SubType实例既分别拥有本身的属性,包括colors,又可使用相同的方法了。

组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优势。成为javascript中最为常见的继承模式。并且instanceof和isPrototypeOf方法也能用于识别组合模式建立的对象。

别的继承模式

继承模式是有不少,上面只是说到咱们常用到的继承模式。包括还有原型式继承、寄生式继承、寄生组合式继承等,其实,只要理解了原型、原型链、构造函数等对象的基本概念,理解起来这中模式都是很是容易的。后续若是有时间,再给你们总结吧~

交流

扫码关注个人我的微信公众号,分享更多原创文章。点击交流学习加我微信、qq群。一块儿学习,一块儿进步


欢迎兄弟们加入:

Node.js技术交流群:209530601

React技术栈:398240621

前端技术杂谈:604953717 (新建)

相关文章
相关标签/搜索