慢慢认识JavaScript面向对象(二)深入原型对象

JavaScript面向对象——原型对象

前面我们讨论过了使用使用工厂模式创建对象,使用构造函数创建对象,再到使用原型模式创建对象。
我们发现了使用比较理想的构造函数来创建对象也会有问题,最后我们使用原型模式来创建对象。
一说到原型模式,原型对象肯定要有了解哦。下面我们一起来学习一下原型对象吧!

深入理解原型模式

1.什么是原型?

  • 我们创建的每个函数都有一个 prototype(原型)属性。
  • 这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。(通俗一点就是在原型对象里添加的属性方法,可以在new出来的实例中共享使用。后面会有详细介绍。

原型对象的作用:

  • 使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
  • 换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

接下来我们来学习一下原型对象的使用:

        function Person() {
        }

        Person.prototype.name = 'alon';
        Person.prototype.age = 29;
        Person.prototype.job = 'web developer';
        Person.prototype.sayName = function () {
            alert(this.name);
        }

        var person1 = new Person();
        person1.sayName();//"alon"

        var person2 = new Person();
        person2.sayName(); //"alon" 
        
        alert(person1.sayName == person2.sayName); //true

代码解析:

  • 在此,我们将 sayName()方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数。
  • 即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。

其实上面内容都是上一节最后讲的,就当复习了吧!接下来才是这一章节的重点,打起精神。

2.深入原型对象

写在前面,要分清楚三个东西,构造函数(Person),原型对象,实例(person1、person2)。谁有什么谁干什么,分清楚。

原型对象的创建:

  • 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。

原型对象上的constructor属性:

  • 默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
  • 用我们上面的例子来说, Person.prototype.constructor指向Person。
  • 也就是原型对象自身来说, 只有一个constructor属性, 而其他属性可以由我们添加或者从Object中继承。

新的实例创建时, 原型对象在哪里呢?

  • 当调用构造函数创建一个新实例后,该实例的内部将包含一个内部属性,该属性的指针, 指向构造函数的原型对象。
  • 这个属性是__proto__。简单说, 每个实例中, 其实也会有一个属性, 该属性是指向原型对象的。
// 原型对象中自身带有一个属性: constructor属性
// 属性指向Person函数
console.log(Person.prototype.constructor); // Person函数

// 对象实例也有一个属性指向原型
console.log(person1.__proto__); // 原型对象
console.log(Person.prototype); // 原型对象
console.log(person1.__proto__ === Person.prototype); // true

我们通过一个图(很丑的手画图)来解释上面的概念:
慢慢认识JavaScript面向对象(二)深入原型对象_第1张图片
再来通过一张比较正式的图来加深大家理解:
慢慢认识JavaScript面向对象(二)深入原型对象_第2张图片
解析:

  • 上面的图解析了构造函数Person、Person的原型对象属性以及Person现有的两个实例之间的关系。
  • Person.prototype指向原型对象, 而Person.prototype.constructor又指回了Person。
  • 原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。
  • Person的每个实例——personl和person2都包含一个内部属性__proto__,该属性也指向原型对象。

当你要获取对象中的属性时,对象搜索属性和方法的过程:

  • 每当代码读取某个对象的某个属性时,都会执行搜索,也就是要找到给定名称的属性。
  • 搜索首先从对象实例本身开始
    • 如果在实例中找到了具有给定名字的属性,则返回该属性的值。
  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
    • 如果在原型对象中找到了这个属性,则返回该属性的值。
  • 也就是说,在我们调用personl.sayName()的时候,会先后执行两次搜索。

可以通过__proto__来修改原型的值(通常不会这样修改, 知道即可)

补充说明:

  • 当我们给person1.name进行赋值时, 其实在给person1实例添加一个name属性。
  • 这个时候再次访问时, 就不会访问原型中的name属性了。
var person1 = new Person()
var person2 = new Person()

person1.sayHello() // alon
person2.sayHello() // alon

// 给person1实例添加属性
person1.name = "Kobe"
person1.sayHello() // Kobe, 来自实例
person2.sayHello() // alon, 来自原型

通过hasOwnProperty判断属性属于实例还是原型。

alert(person1.hasOwnProperty("name")) // true
alert(person2.hasOwnProperty("name")) // false

3.简洁的原型语法

简洁语法概述:

  • 如果按照前面的做法, 每添加一个原型属性和方法, 都要敲一遍Person.prototype.来添加。这样会显得很麻烦。
  • 为了减少不必要的输入, 另外也为了更好的封装性, 更常用的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。

字面量重写原型对象:


        // 定义Person构造函数
        function Person() { }

        // 重写Person的原型属性
        Person.prototype = {
            name: "alon",
            age: 18,
            height: 1.88,

            sayHello: function () {
                alert(this.name)
            }
        }

        创建Person对象
        var person = new Person()

        console.log(person.constructor)
        alert(person.constructor === Object) // true
        alert(person.constructor === Person) // false

        alert(person instanceof Person) // true

这里说一下 instanceof() :instanceof的普通的用法,obj instanceof Object 检测Object.prototype是否存在于参数obj的原型链上。

注意:

  • 我们将Person.prototype赋值了一个新的对象字面量, 最终结果和原来是一样的。
  • 但是: constructor属性不再指向Person了。
  • 前面我们说过, 每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性。
  • 而我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是Person构造函数了。

如果在某些情况下, 我们确实需要用到constructor的值, 可以手动的给constructor赋值即可。

// 定义Person构造函数
function Person() {}

// 重写Person的原型属性
Person.prototype = {
    constructor: Person,
    name: "alon",
    age: 18,
    height: 1.88,

    sayHello: function () {
        alert(this.name)
    }
}

// 创建Person对象
var person = new Person()

alert(person.constructor === Object) // false
alert(person.constructor === Person) // true

alert(person instanceof Person) // true

上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被设置了true。

  • 默认情况下, 原生的constructor属性是不可枚举的。
  • 如果希望解决这个问题, 就可以使用我们前面介绍的Object.defineProperty()函数了。
// 定义Person构造函数
function Person() {}

// 重写Person的原型属性
Person.prototype = {
    name: "alon",
    age: 18,
    height: 1.88,

    sayHello: function () {
        alert(this.name)
    }
}

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

4.修改原型属性

我们来看下面的代码会不会有问题:

// 定义Person构造函数
function Person() {}

// 创建Person的对象
var person = new Person()

// 给Person的原型添加方法
Person.prototype = {
    constructor: Person,
    sayHello: function () {
        alert("Hello JavaScript")
    }
}
// 调用方法
person.sayHello()

代码解析:

代码是不能正常运行的。 最初我们创建的person实例指向的是原来的原型对象, 原来的原型对象里没有sayHello()方法。实例创建出来之后,Person的prototype才指向了一个新的对象,在新的对象里才含有sayHello()方法。

5.原型对象问题

原型对象也有一些缺点:

  • 首先, 它不再有为构造函数传递参数的环节, 所有实例在默认情况下都将有相同的属性值。
  • 另外, 原型中所有的属性是被很多实例共享的, 这种共享对于函数来说非常适合, 对于基本属性通常情况下也不会有问题. (因为通过person.name直接修改时, 会在实例上重新创建该属性名, 不会在原型上修改. 除非使用person. __proto__.name修改)。
  • 但是, 对于引用类型的实例, 就必然会存在问题。

考虑下面代码的问题:

// 定义Person构造函数
function Person() {}

// 设置Person原型
Person.prototype = {
    constructor: Person,
    name: "alon",
    age: 18,
    height: 1.88,
    hobby: ["Basketball", "Football"],

    sayHello: function () {
        alert("Hello JavaScript")
    }
}

// 创建两个person对象
var person1 = new Person()
var person2 = new Person()

alert(person1.hobby) // Basketball,Football
alert(person2.hobby) // Basketball,Football

person1.hobby.push("tennis")

alert(person1.hobby) // Basketball,Football,tennis
alert(person2.hobby) // Basketball,Football,tennis

OK, 我们会发现, 我们明明给person1添加了一个爱好, 但是person2也被添加到一个爱好。

因为它们是共享的同一个数组。

但是, 我们希望每个人有属于自己的爱好, 而不是所有的Person爱好都相同。

那就有如下方法。

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

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

组合构造函数和原型模式的代码:

// 创建Person构造函数
        // 创建Person构造函数
        function Person(name, age, height) {
            this.name = name
            this.age = age
            this.height = height
            this.hobby = ["Basketball", "Football"]
        }

        // 重新Peron的原型对象
        Person.prototype = {
            constructor: Person,
            sayHello: function () {
                alert("Hello JavaScript")
            }
        }

        // 创建对象
        var person1 = new Person("alon", 18, 1.88)
        var person2 = new Person("Kobe", 30, 1.98)

        // 测试是否共享了函数
        alert(person1.sayHello == person2.sayHello) // true

        // 测试引用类型是否存在问题
        person1.hobby.push("tennis")
        alert(person1.hobby) //Basketball,Football,tennis
        alert(person2.hobby) //Basketball,Football

这个就解决了使用引用类型的实例存在的问题。

向着宏大的目标努力前进~

文章参考借鉴:https://mp.weixin.qq.com/s/TeBnVpvb_sewv3np7TKc-Q

你可能感兴趣的:(JavaScript原型对象,vue.js,javascript,前端,设计模式,js)