JavaScript | 对象和继承

1、 建立对象的几种方式数组

  1. 工厂模式

工厂模式就是:定义一个“工厂函数”,每次调用这个函数就会获得一个对象。工厂模式建立的对象,是一个函数结果而不能肯定类型。app

function createPerson(name,age){
    var o = new Object()
    o.name=name
    o.age=age
    return o
}

var p = createPerson('hhh',12)
//这时候p就至关于
{
    name:'hhh',
    age:12
}
//可是P是 没有一个类型可言的。若是用typeof,只能是Object类型、

2.构造函数模式函数

构造函数构造函数,顾名思义,若是你还记得原型链的话,应该知道,每一个对象实例都有一个__proto__属性,指向的是它们的原型。而原型里有一个属性是 constructor ,这个属性其实就是这个构造函数。也就是说:测试

经过构造函数生成对象,其实就是先写一个构造函数,这个构造函数的里面的this,指的实际上是这个函数的prototype,即原型。构造函数定义好,那经过new这个构造函数,就能够建立这个构造函数的原型的孩子,即对象实例。this

//定义构造函数
function Person(namem,age){
    this.name= name;
    this.age= age;
    //想一下这里面的this指向的是谁?
    //原型链:每一个函数都有一个prototype属性,每一个对象都有一个__proto__属性
    //指向的 即是 `原型`
    //因此,这里的this,其实指向的就是Person这个函数的原型:Person.prototype
    this.sayHello=function(){
        console.log("hello")
    }
}
//生成对象实例
var p = new Person('hhh',12)

//这里,p.__proto__=== Person.prototype
//那你想一下,实例对象p的原型的构造函数(constructor)又是谁呢?
//就是Person()

这时候实例对象p是有类型可言的。它就是Person类型。prototype

这里就出现了一个新规则:凡是经过new来调用的函数,会被看成构造函数。不然就是普通函数。指针

构造函数的缺点就是:每次生成 一个对象实例,至关于调用一次构造函数,每次调用构造函数,里面的方法都会被从新建立一遍,形成资源浪费。(怎么证实每一个建立的对象实例里面的方法都是不等价的?)code

//1.首先,this.sayHello=function(){}这句话实际上是:
var tmp = new Function()
this.sayHello= tmp
//因此,每次调用构造函数,都会建立一个函数实例(即Function类型的对象实例)

//2.其次,经过测试
p1 = new Person('1',12)
p2 = new Person('2',21)
console.log(p1.sayHello===p2.sayHello)//结果是false

这个问题能够经过把函数定义到外面来解决。在构造函数外面只声明一次函数。对象

可是新问题又来了,这个声明的函数就和对象、类型没什么关系了。继承

3.原型模式

由于每一个建立的函数都有一个prototype属性。这个属性是一个指针,指向的是一个对象。也就是父亲。

这个父亲所拥有的全部的属性和方法,均可以被孩子继承。那么,给这个父亲添加属性和方法,其实也在给孩子添加属性和方法。

function Person(){
    //这是构造孩子的构造函数
}
Person.prototype.name = 'tmpname'
Person.prototype.age = 0
Person.protytype.sayHello = function(){
    console.log('hello')
}

var p1 = new Person()
var p2 = new Person()

//看这两个对象实例,就是经过构造函数建立的孩子,他们其实都有name和age属性
//p1和p2均可以访问name 和 age,可是都是原型中的属性值
//若是咱们给属性值从新赋值,其实不是改变值,而是覆盖掉孩子继承的这个属性

p1.name='ware'

//这句话的意思是,给p1一个name属性,值为ware,然而这个属性因为和原型中的属性同名
//则会覆盖掉原型中的这个属性,而不是修改掉原型中的这个属性值
//若是咱们想从新访问原型中的属性值,只须要把这个属性delete掉就能够了

delete p1.name

hasOwnProperty() 方法,能够检测对象的属性是本身的仍是继承自原型的

3.1 in操做符

in 操做符 在 经过对象可以访问到给定属性时 返回true

console.log('name' in p1) //true

同时使用hasOwnProperty()和In操做符可以肯定属性是存在于对象中仍是原型中:

function whereProperty(obj,pro){
    console.log(!obj.hasOwnProperty(pro)&&(pro in obj)?'在原型里':'在对象里')
}

in 操做符能够和for联合使用,用来遍历对象全部能访问到的(包括原型中的) 可枚举(enumerated)属性。

经过keys()方法,也能够达到相似效果。这个方法返回一个数组。

3.2 原型模式的简写

前面的例子能够简写:

function Person(){
    
}

Person.prototype = {
    name:'tmpName',
    age:0,
    sayHello:function(){
        console.log('hello')
    }
}

可是这样的写法,至关于给Person的原型赋值了,而原来的写法只是给Person的原型添加属性。这是两种概念。

默认的,咱们建立一个函数,同时会建立它的prototype对象。而这个函数自己,就是原型对象的construtor。

可是这样的简写方式,至关于覆盖掉了默认的prototype对象。因此,既然覆盖掉了,而咱们重写的时候,这个原型对象就没有construtor属性,那就会从Object类里面继承,由于

{
    name:'tmpName',
    age:0,
    sayHello:function(){
        console.log('hello')
    }

自己是一个Object类型的对象。

若是咱们但愿之后经过这个构造函数建立的对象实例,能够访问construtor,而且指向的是Person,那咱们就应该在从新给原型赋值的时候,带上constructor属性。

Person.prototype = {
    constructor:Person,
    name:'tmpName',
    age:0,
    sayHello:function(){
        console.log('hello')
    }
}

不过直接写明,会让constructor属性变为可枚举的。若是想要原来不可枚举的效果,用Object.defineProperty() 这个方法。

Object.defineProperty(Person.prototype,'constructor',{
    enumerable:false,
    value:Person
})

对原型的操做(好比添加属性、方法)是动态的,无论孩子是何时建立的,只要父亲变了,孩子就会跟着变。

原型模式的缺点就是:全部的孩子在建立时,会有统一的属性及属性值。也就是说,没有定制性了。

  1. 混合模式

所谓混合就是:构造函数定义和原型自定义两种模式的混合。

构造函数定义,定义的是什么?是当前构造函数可生成的实例的属性和方法。

原型定义,定义的是什么? 是原型的属性和方法,共享于每一个实例。

构造+原型、动态原型、寄生构造、稳妥构造 四种方式。寄生构造模式只须要了解,用处不大。稳妥构造方式其实就是封装对象属性。

2、继承

若是原型链没有任何问题的话,继承其实就是:全部的实例继承其原型,或其原型链上面的全部父原型。

可是,不凑巧,原型链有个问题。

原型中定义的属性,会被全部实例共享,除非实例对象里覆盖掉这个属性。——这是对于基本数据类型而言。

原型中定义的“引用类型值”的属性,会被全部实例共享。

那什么是“引用类型值” 呢?

ECMAScript 变量可能包含两种不一样数据类型的值:基本类型值和引用类型值。基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。引用类型的值是保存在内存中的对象。与其余语言不一样,JavaScript 不容许直接访问内存中的位置,也就是说不能直接操做对象的内存空间。在操做对象时,其实是在操做对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。

也就是说,咱们若是在原型里定义一个属性——数组类型的。那孩子继承的这个属性实际上是这个属性的引用。更改孩子中这个数组,意味着更改引用。

2.1 借用构造函数

借用构造函数,其实就是在 孩子的构造函数中,调用父亲(原型)的构造函数。这样,就把父亲构造函数中定义的全部代码都在子类的构造函数中执行了一遍。

function Father(){
    this.color=[1,2,3]
}

function Child(){
    Father.call(this)
}

//这时候用Child new一个对象实例,那对象实例就拥有了color这个属性,并且是独自拥有color的拷贝。

call 和 apply 讲解 改变当前做用域中this对象。

这种继承方式有构造函数模式的问题:方法都定义在构造函数里,不可复用且资源浪费。

2.2 组合继承

组合继承 其实就是应用个 混合模式中的原型+构造函数模式。

function Father(name){
    this.name = name
    this.color = ['red','yellow','blue']
}

function Child(name,age){
    Father.call(this,name)
    this.age=age
}

Father.prototype.sayHello=function(){
    console.log(this.name)
}

//1.建立父构造函数  
//2.建立子构造函数并继承父类中的name属性
//3.给父类型的原型添加一个方法sayHello

Child.prototype = new Father()//给子类型添加一个原型,这个原型就是父类型的实例
Child.prototype.constructor = Child//肯定经过子类型生成的实例对象 是Child类型

//到这里,全部经过new Child()建立的对象实例,都拥有了sayHello方法,各自拥有color/name/age属性

2.3 原型式继承/寄生式继承

颇有意思的一个想法,道格拉斯·克罗克福德在2006年提出来的。咱们不急去了解它,先整理一下思路:按前面那些方式,到底建立一个继承于父类的对象实例的本质是什么?

本质很简单:按照父亲建立出孩子。不只要保证每一个孩子有本身的个性,还要保证每一个孩子同样的地方不须要重复创造,并且单个孩子的某个动做,不会影响到父亲以致于波及到其余孩子。

逐条分析:

  1. 保证每一个孩子有本身的个性:孩子的构造函数就是干这个事的。每一个孩子有本身的独有属性,那这些独有属性就在构造函数里写。其余的都在父亲(原型)里继承。
  2. 保证孩子同样的地方不须要重复创造:每一个孩子都会说话、吃饭、睡觉,这些没必要要在孩子的构造函数里写,只须要在父亲(原型)里写就能够了。
  3. 不会影响到父亲波及其余孩子:引用类型值的属性。这些属性若是是继承的,那一个孩子更改了这个属性,这个父亲的全部孩子都会改变了。由于全部的孩子里的这个属性,都是引用,而不是值。

因此前面才会有这些继承方式,这些建立对象的方式。

道格拉斯这位兄弟有一天突发奇想,这世界上某个对象了,那经过现有的这个对象,是否是能够直接建立新对象?

function child(FatherIns){
    function F(){}
    F.prototype = FaherIns
    return new F()
}

//本质是建立一个把FahterIns看成原型的  构造函数
//而后经过这个构造函数建立一个孩子

其实这种继承方式的本质是:对象的深拷贝。而并不是严格的继承。因此,这种继承方式的前提是:1.有现成继承的对象,2.不须要考虑类型 3.现有对象中若是存在引用类型值属性,将会被全部孩子继承。

因而,ES5为此给Object增添了一个新方法:Object.create()用来建立新对象,接收两个参数:1.用做新对象原型的对象,2.一个为新对象定义额外属性的对象。

而后,道哥又想,能不能给生成的对象添加方法呢?

而后就:

function child(fatherObj){
    var tmp = Object.create(fatherObj,{
        childPro:{
            value:'xxxxx'
        }
    })
    tmp.childMethod = function(){
        ...
    }
    return tmp;
}

这特么就是寄生式继承。

2.4 寄生组合式继承

你觉得道哥思想的影响真的就这么简单么?然而并非。回看一下组合式继承。

组合式继承的思路:

  1. 建立子类型的构造函数。

  2. 在构造函数中,调用父类的构造函数。

  3. 定义完构造函数以后,外面还要给子类型指定原型:Child.prototype = new Father()
  4. 咱们都知道指定原型形成的弊端就是失去constructor。因此再指定一下constructor. Child.prototype.constructor = Child
  5. 这时候继承定义完成。

这时候咱们发现,Father()这个构造函数调用了两次啊。并且,Child的prototype咱们实际上是不关心它的类型的。而且,Child.prototype可不能够从一个现有的对象建立呢?彻底能够啊。那这个现有的对象就是Father.prototype啊。

因此咱们就能够把三、4步写成:

var prototype = Object.create(Father.prototype)
Child.prototype = prototype
prototype.constructor = Child

看,这里并无给Child一个经过Father()新建的实例,而是经过Father.prototype拷贝的实例。由于这个实例的类型并非咱们关心的。

相关文章
相关标签/搜索