JavaScript 中的原型原来是这样的

什么是原型

原型其实就是一个特殊的对象,在声明函数的时候自动创建的。

比如,我们现在声明一个构造函数 A ,除了会申请保存函数的内存空间,还会额外申请一个内存空间,用于存储构造函数 A 的原型对象。所有函数中(Function.prototype.bind 除外)默认都有一个 prototype 的属性,它保存了函数的原型对象的地址(引用)(也就是它指向了原型对象)。
而在原型对象中默认有一个 constructor 属性存储了构造函数的地址(引用)(也就是 constructor 指向了构造函数)。如果不理解上面所说的,那我们看下面的图:

原型

浏览器控制台中:

原型

_ _proto_ _prototype

刚开始接触原型的时候这两个东西很容易就搞混了。

先记住以下两点,就很容易就区分了:

  • prototype 是函数中才有的属性
  • __proto__ 是所有对象都有的属性

 _ _proto_ _ 与 prototye

我们已经知道了函数中的 prototype 属性指向的是它的原型对象,那么对象中的 __proto__ 代表什么?

一般情况下,对象中的 __proto__ 属性是指向它的构造函数的原型对象的,即和构造函数中的 prototype 属性所指向的对象是同一个对象。

用一段简单的代码:

function A() {}
var a = new A()

上图看着不够简便,我们简化一下:

还有一点,__proto__ 不是一个规范属性,ie(除了 ie10) 不支持。对应的标准属性是 [[Prototype]] ,但是这个属性我们没法直接访问到。开发者尽量不要用这种方式去访问,因为操作不慎会改变这个对象的继承原型链。

在使用 Object.create(参数) 方式创建对象时,对象的 __proto__ 属性指向的是传入的参数。

原型链

由于 __proto__ 是所有对象都具有的属性,而 __proto__ 本身指向的原型(函数.prototype)也是一个对象,它也有 __proto__ 属性。所以这样会形成由 __proto__对象原型连起来的链条。这就是原型链。原型链的顶端是 Object.prototype(Object 是所有对象的祖宗) ,Object.prototype.__proto__的值为 null

还是看之前的代码:

function A() {}
var a = new A()

它的原型链如下:
原型链

构造函数 A 其实也是一个对象。所有函数都是由 Function 函数构造的。(声明函数 function A() {} 等价于 var A = new Function()) 。所以所有函数的 __proto__ 指向的都是 Function.prototype 。更新上图:
原型链

Function 也是一个函数,它的 __proto__ 指向的也是 Functon.prototypeFuntion.__proto__ === Function.prototype。继更新上图:

原型链

Object 同样是一个函数,所以 Object.__proto__ === Function.prototype

到了这里,我们应该可以看懂下面这张图了:

原型链

原型的作用

当 JS 引擎查找对象属性时,先查找对象本身是否存在该属性,如果不存在,会在对象的 __proto__ 里找,还找不到就会沿着原型链一直找到原型链顶端(Object.prototype) 直到找到属性为止,最后在原型链顶端都没找到就返回 undefined

由于上面的机制,原型的作用就很明显了——共享属性,节省内存空间。

function Animal() {
this.name = '动物'
this.eat = function() {
console.log('在吃···')
}
}
var a1 = new Animal()
var a2 = new Animal()

console.log(a1.eat === a2.eat) // false
// 每个对象的 eat 方法不是同一个,但方法类容一样,浪费内存

使用原型解决:

function Animal(name) {
this.name = '动物'
}
Animal.prototype.eat = function() {
console.log('吃')
}

var a1 = new Animal()
var a2 = new Animal()

console.log(a1.eat === a2.eat) //true
// a1.eat 和 a2.eat 都同是一个方法(Animal.prototype.eat)

原型非常适合封装共享的方法。但是上面的代码把构造函数和原型分开写了。封装不到位。使用动态类型模式解决。

function Animal() {
this.name = '动物'

/*
判断 this.eat 是不是 函数类型,
如果不是,则表示是第一次创建对象或者调用 Animal 函数,
会将 eat 添加到原型中去。
如果是,则表示原型中存在了 eat 方法,不需要再添加。
*/
if(typeof this.eat !== 'function') {
Animal.prototype.eat = function() {
console.log('吃')
}
}
}

var a = new Animal()
a.eat()

原型基于之前的共享属性和方法,是实现 JS 中继承的基础。

与原型有关的方法

hasOwnProperty()

通过之前的学习,我们知道了去访问一个对象的属性时,会在原型链上查找。所以我们并不知道这个属性来自哪里。

hasOwnProperty() 方法返回一个布尔值,可以判断一个属性名是否在自对象本身上。

function Animal() {}
Animal.prototype.name = '动物'
var a = new Animal()
a.age = 3

console.log(a.hasOwnProperty('name')) // false
console.log(a.hasOwnProperty('age') // true

in 操作符

in 操作符用返回一个布尔值,用来判断一个属性名能否在对象上找到。在对象的原型链上找到也返回 true

function Animal() {}
Animal.prototype.name = '动物'
var a = new Animal()
a.age = 3

console.log('name' in a) // true
console.log('age' in a) // true
console.log('sex' in a) // false

总结

  • 原型就是一个对象,声明函数就会创建原型对象
  • prototype 只存在于函数中
  • 所有对象都有一个 __proto__ 属性,它指向对象的构造函数的原型
  • 原型也是对象,也有 __proto__ 属性,__proto__ 将对象和原型连接起来,形成原型链
  • Object.prototype 是原型链的顶端
  • 访问对象的属性会沿着对象的原型链找下去
  • 原型可以共享属性和方法,是继承的基础
© 2019 墨夜 All Rights Reserved.
Theme by hiero