JavaScript | 对象和继承

一、 创建对象的几种方式

  1. 工厂模式

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

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这个构造函数,就可以创建这个构造函数的原型的孩子,即对象实例。

//定义构造函数
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类型。

这里就出现了一个新规则:凡是通过new来调用的函数,会被当作构造函数。否则就是普通函数。

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

//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. 混合模式

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

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

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

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

二、继承

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

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

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

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

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

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啊。

所以我们就可以把3、4步写成:

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

看,这里并没有给Child一个通过Father()新建的实例,而是通过Father.prototype拷贝的实例。因为这个实例的类型并不是我们关心的。

原文地址:https://www.cnblogs.com/warmer/p/9903941.html