JavaScript之原型(2) | 8月更文挑战

371 阅读5分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

原型

内容引导:

  • 使用 prototype 原型对象解决构造函数的问题
  • 分析 构造函数、prototype 原型对象、实例对象 三者之间的关系
  • 属性成员搜索原则:原型链
  • 实例对象读写原型对象中的成员
  • 原型对象的简写形式
  • 原生对象的原型:Object、Array、String...
  • 原型对象的问题
  • 构造的函数和原型对象使用建议

更好的解决方案: prototype

JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。 这个对象的所有属性和方法,都 会被构造函数的所拥有。这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。

function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person(...)
var p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true

这时所有实例的 type 属性和 sayName() 方法, 其实都是同一个内存地址,指向 prototype 对象,因此就提 高了运行效率。

构造函数、实例、原型三者之间的关系

JavaScript高级.jpg

任何函数都具有一个 prototype 属性,该属性是一个对象。

function F () {}
console.log(F.prototype) // => object
F.prototype.sayHi = function () {
console.log('hi!')
}

构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。

console.log(F.prototype.constructor === F) // => true

通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 proto

var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true

__proto__ 是非标准属性。 实例对象可以直接访问原型对象成员。

instance.sayHi() // => hi! 总结:

  • 任何函数都具有一个 prototype 属性,该属性是一个对象
  • 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
  • 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 proto
  • 所有实例都直接或间接继承了原型对象的成员

属性成员的搜索原则:原型链

了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的 成员。

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性

  • 搜索首先从对象实例本身开始
  • 如果在实例中找到了具有给定名字的属性,则返回该属性的值
  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
  • 如果在原型对象中找到了这个属性,则返回该属性的值

也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜索:

  • 首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
  • ”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
  • ”于是,它就读取那个保存在原型对象中的函数。
  • 当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。 而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

总结:

  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回 undefined

实例对象读写原型对象成员

读取:

  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回 undefined 值类型成员写入( 实例对象.值类型成员 = xx ):
  • 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
  • 也就是说该行为实际上会屏蔽掉对原型对象成员的访问

引用类型成员写入( 实例对象.引用类型成员 = xx ):

  • 同上

复杂类型修改( 实例对象.成员.xx = xx ):

  • 同样会先在自己身上找该成员,如果自己身上找到则直接修改
  • 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
  • 如果一直到原型链的末端还没有找到该成员,则报错( 实例对象.undefined.xx = xx )

更简单的原型语法

我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。 为减少不必要的输入,更常见 的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:

function Student(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
}
//在Student的原生对象中添加了一个方法
/* Student.prototype.study=function(){
//在Student的原生在添加一个属性
//Student.prototype.schoolName='123';
Student.prototype={
constructor:Student,//=>手动将 constructor 执行正确的构造函数
schoolName:"123",
study:function(){
console.log(this.name+"在马桶上看英语书");
}
}

在该示例中,我们将 Person.prototype 重置到了一个新的对象。 这样做的好处就是为 Person.prototype 添 加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员。 所以,我们为了保持 constructor 的指向正确,建议的写法是:

function Student(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
}
//在Student的原生对象中添加了一个方法
/* Student.prototype.study=function(){
console.log(this.name+"在马桶上看英语书");
}*/
//在Student的原生在添加一个属性
//Student.prototype.schoolName='123';
Student.prototype={
constructor:Student,//=>手动将 constructor 执行正确的构造函数
schoolName:"123",
study:function(){
console.log(this.name+"在马桶上看英语书");
}
}

原生对象的原型

所有函数都有 prototype 属性对象。

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • Date.prototype
  • ...

原型对象使用建议

  • 私有成员(一般就是非函数成员)放到构造函数中
  • 共享成员(一般就是函数)放到原型对象中
  • 如果重置了 prototype 记得修正 constructor 的指向