42

原型

原型还是比较重要的,想单独抽出一章来细说,说到原型,那么什么是原型呢?

在构造函数创建出来的时候,都有一个prototype(原型)属性,这个属性是一个指针,系统会默认的创建并关联一个对象,这个对象就是原型,原型对象默认是空对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
说白了就是可以在构造函数上调用prototype属性来指向原型,从而创建那个对象实例的原型对象

使用原型有什么好处呢?

使用原型的好处是可以让所有对象实例共享它所包含的属性和方法。

转晕了麽?是不是超级乱?🤔又构造函数,又原型,又实例,不担心,我一句话点破你
我们所有的构造函数最终都要演变成实例才有意义,因为在构造函数中定义方法无法被所有的实例共享,因此只能找构造函数的上一级,就是原型,在原型上定义的属性和方法可以被所有的实例所共享,这就是对原型对象的性质

看个图你就知道了,它们三者之间就是三角恋关系👪
图片描述
很通俗易懂了吧
构造函数.prototype = 原型
原型.constructor = 构造函数
实例对象.constructor = 构造函数(这是因为实例对象在自身找不到constructor属性,那么他会通过__proto__去原型中找,通过原型搭桥指向了构造函数)
实例对象.__proto__ = 原型

原型是打印显示不出来的,只能通过 构造函数.prototype 去表示

下面介绍另外两个获取原型的方法🎁

isPrototypeOf()方法:用于判断这个实例的指针是否指向这个原型。
Object.getPrototypeOf()方法:获取实例的原型,这个方法支持的浏览器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome,因此比较推荐通过这个方法获取对象的原型。
假定有个Person构造函数和person对象
Person.prototype.isPrototypeof(person)  // 返回true说明前者是person1的原型
Object.getPrototypeOf(person) === Person.prototype // 获取person的原型
多个对象实例共享原型所保存的属性和方法的基本原理
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性值。

我们可以访问原型中的值,但是却不能重写原型中的值,如果我们在实例中添加一个属性,而这个属性名和原型中的重名,则这个属性将会屏蔽(复写)原型中的那个属性。

function Person() {}

Person.prototype.name = "George"
Person.prototype.sayName = function() {
    console.log(this.name)
}

let person1 = new Person();
let person2 = new Person();
person1.name = "命名最头痛";

// 在这一环节,person1.name会从他实例中找,若实例没找到,则继续搜索它的原型对象
console.log(person1.name); // 命名最头痛 
console.log(person2.name); // George

在实例对象中添加一属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接

若想完全删除实例属性,可使用delete操作符,从而让我们能够重新访问原型中的属性。

delete 操作符的使用

依旧用上面那个例子
delete操作符可用于删除对象的属性,无论是实例上的属性,还是在原型上的属性都可以删

delete person1.name    // 删除这个实例的属性
delete Person.prototype.name    // 删除原型上的属性
delete Person.prototype.constructor // 删除constructor属性,这样就没办法指回函数了
hasOwnProperty()方法可用来检测一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例时,才返回true,也可以这样理解,hasOwnProperty方法用于检测这个属性是否是对象本身属性。
obj.hasOwnProperty('属性名')

Demo:

function Person(){ 
  this.name = '命名最头痛'
}
var person = new Person()
Person.prototype.age = '18'
console.log(person.hasOwnProperty('name'))  // true
console.log(Person.prototype.hasOwnProperty('age')) // true

in操作符

in操作符有两种用法
①放在for-in循环中使用,for-in能够返回所有能够通过对象访问的、可枚举的(enumerable)属性(可枚举属性可参看一眼看穿👀JS对象中对象属性部分)其中,既包括存在于实例中的属性,也包括存在于原型中的属性,屏蔽了所有不可枚举的属性
②单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。说白了就是,可以通过in操作符判断这个对象是否有这个属性。in操作符会先在实例中寻找属性,寻找无果再去原型中寻找,但不可以逆向查找。

举个🌰(in 单独使用)

function Person(){}
var person = new Person()
// name存在于实例中
person.name = '命名最头痛'
// age存在于原型中
Person.prototype.age = '18'
console.log('name' in person)  // true
console.log('name' in Person.prototype) // false 不可逆向查找,原型中不能找到实例中属性
console.log('age' in person)  // true
console.log('age' in Person.prototype) // true

我们可以构造一个函数,用于判断一个属性是在原型中还是在对象实例中,同时使用in和hasOwnProperty()方法可以实现这个函数的构造

// 判断一个属性是否在原型中
function hasPrototypeProperty(object,name) {
    return (name in object) && !object.hasOwnProperty(name)
}

for-in、Object.keys()和Object.getOwnPropertyNames()

for-in和Object.keys都是用于遍历可枚举对象属性的,他们的主要区别在于
for-in:遍历对象上所有可枚举的属性,包括原型上的(会在原型上寻找)
Object.keys():只遍历自身的可枚举属性,返回一个属性数组(不会在原型上寻找)

Object.getOwnPropertyNames:获取自身所有属性,无论它是否可枚举

function Person() {}
let person = new Person()
// 实例上定义属性
Object.defineProperties(person, {
  name: {
    value: '命名最头痛',
    enumerable: true
  },
  age: {
    value: 18
  }
}) 
// 原型上定义属性
Object.defineProperties(Object.getPrototypeOf(person), {
  year: {
    value: 2018,
    enumerable: true
  },
  job: {
    value: 'programer'
  },
  demo: {
    enumerable: true,
    get() {
      return this.year
    }
  }
})
for(let i in person) {
  console.log(i, person[i]) 
  // name 命名最头痛
  // year 2018
  // demo 2018
}
for(let i in Object.getPrototypeOf(person)){
  console.log(i, Object.getPrototypeOf(person)[i])
  // year 2018
  // demo 2018
}
console.log(Object.keys(person)) // ['name']
console.log(Object.getOwnPropertyNames(person)) // ["name", "age"]
console.log(Object.getOwnPropertyNames(Person)) // ["length", "name", "prototype"]
console.log(Object.getOwnPropertyNames(Person.prototype)) // ["constructor", "year", "job", "demo"]

使用字面量方式创建函数原型

上面我们介绍了通过构造函数方式创建函数原型,下面介绍一种通过字面量的方式创建函数原型的方法

function Person() {}
Person.prototype = {
    name: '命名最头痛',
    age: 18,
    sayName: function(){
        console.log(this.name)
    }
}

虽然这种创建方式相对于构造函数方式在视觉上有更好的封装性,但是这种方式创建对象有个例外,那就是constructor属性不再指向Person了,因为每创建一个函数,就会同时创建它的prototype对象,这个对象会自动获得constructor属性。而字面量这种写法,会重写默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数)不再指向Person函数了,此时,尽管instanceof操作符还能返回正确的结果,但通过constructor却无法确定对象的类型。

function Person() {}
let person = new Person()
console.log(person instanceof Object) // true
console.log(person instanceof Person) // true
console.log(person.constructor === Person) // false
console.log(person.constructor === Object) // true

若想让字面量方式创建原型的constructor重新指向Person,只用刻意为它设置值即可

function Person() {}
Person.prototype = {
    constructor: Person,
    name: '命名最头痛'
    ...
}

这样设置还是有个隐患的,因为我们知道,直接定义属性的方式会导致属性特性默认值为true,这样定义的constructor[[Enumerable]]特性被设置为true,而原生的constructor是不可枚举的,因既想实现constructor指向,又想让[[Enumerable]]特性被设置为true,只能通过Object.defineProperty()方法来写constructor

function Person() {}
Person.prototype = {
    name: '命名最头痛',
    ...
}
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
})

原型的动态性

用一句话说明白就是原型的变化可以通过实例反应出来,可以先创建实例,再定义或修改原型上的属性。因为实例与原型之间的连接是一个指针,而非一个副本,因此可以在原型中找到新的属性并返回保存那里的函数。这种动态性会在多个实例中相互影响,即其中一个实例修改了原型上的内容,也能立刻在所有实例中反映出来。这个原因归结为实例与原型之间的松散链接关系

尽管可以随时在原型中添加属性和方法,并且修改的值能够立刻在所有对象实例中反映出来,但是如果重新整个原型对象,结果就不一样了。在调用构造函数时会为实例添加一个指向最初指向原型的[[prototype]]指针,把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系,记住:实例中的指针仅指向原型,而不指向构造函数

function Person() {}
let person = new Person();
Person.prototype = {
  constructor: Person,
  name: '命名最头痛',
  sayName: function(){
    console.log(this.name) // 因为重写完后this的指向就变了
  }
}
person.sayName() // error
console.log(person.name) //undefined

图片描述

也就是说,你可以在原型上定义新属性,但是尽量不要重写原型对象,因为这会导致之前实例无法指新定义的原型。
下面通过代码说明这一现象

function Person() {}
let person = new Person();
Person.prototype.oldName='George'
console.log(person.oldName) // George

Person.prototype = {
  constructor: Person,
  name: '命名最头痛',
  sayName: function(){
    console.log(this.name)
  }
}
// 重写原型对象后,与实例之间的联系被切断了
console.log(person.name) // undefined
 
// 只能生成新实例才能访问新定义的原型
let person1 = new Person();
console.log(person1.name) // 命名最头痛

原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String等等)都在其构造函数的原型上定义了方法。通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。

🌰
为原生对象String添加新方法newWay

String.prototype.newWay = function(val) {
   return val
}

原型对象的问题

原型对象省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,虽然这会在某些情况下会有些不方便,但还不是原型的最大问题,原型模式的最大问题是由其共享本性所导致的。即原型的动态性,其中一个实例修改了原型上的内容,也能立刻在所有实例中反映出来。

共享本性就是双刃剑
如果我们期望,所有实例共享一个属性值,那原型方法可以很好解决这个问题,但是如果我们期望每个实例都有属于自己的属性,把这个属性值定义在原型上就会相互有影响了。

小结

  • 构造函数创建出来,会有一个原型属性prototype,这个属性是一个指针
  • 构造函数,实例,原型三角恋关系图请移至顶部
  • 获得原型的方法
    ①构造函数.prototype
    ②实例.__proto__
    ③Object.getPrototypeOf()方法:该方法用于获取实例的原型,比较推荐,因为大部分浏览器都支持
  • isPrototypeOf()方法:用于判断这个实例的指针是否指向这个原型。
  • delete操作符可以删除对象属性,无论这个属性是在原型上还是实例上
  • hasOwnProperty方法用于检测某一属性是否是对象自身属性。
  • in操作符有两种使用方式
    ①for-in:用于遍历对象可枚举属性,会在原型上搜索
    ②单独使用:判断对象中是否有这个属性,仅限于可枚举属性,返回Boolean类型
  • Object.keys()只遍历自身的可枚举属性,返回一个属性数组
  • Object.getOwnPropertyNames()方法:获取自身所有属性,无论它是否可枚举
  • 每创建一个函数,就会同时创建它的prototype对象,这个对象会自动获得constructor属性。
  • 原型模式的最大问题是由其共享本性所导致的

尾声

函数原型的探索之路任重道远,也是JS重点之一。


命名最头痛
385 声望655 粉丝

a == b ? a : b