面试官:请说一下 JS 中的原型和原型链

793 阅读5分钟

原型和原型链

图片来源网络,仅作配文展示

欢迎关注微信公众号【前端功成屋】分享一些前端技术、面试题、面试技巧

对于使用过基于类的语言(如 Java 或 C++)的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class 实现。(在 ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。

什么是原型

JS 中的对象包含了一个 prototype 的内部属性,这个属性所对应的就是该对象的原型。 每个实例对象(object)都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null

根据定义,null 没有原型,并作为这个原型链的最后一个环节。

这个 __proto__ 称作隐式原型 。prototype 称作显式原型。

小结

  1. 所有引用类型(函数,数组,对象)都拥有 __proto__ 属性(隐式原型)
  2. 所有函数除了有 __proto__ 属性之外还拥有 prototype 属性(显式原型)
  3. 原型对象:每创建一个函数,该函数会自动带有一个 prototype 属性,该属性是一个指针,指向一个对象,我们称之为原型对象。
  4. 函数除了有 __proto__ 属性之外还拥有 prototype 属性,我们可以借助构造函数来说明下
function A() {
    this.name = ‘tom’;
    this.age = 12;
}
A.prototype = {
    num: 12
}
var a = new A()
  1. 实例对象 a 只有 __proto__ (隐式原型),构造函数既有 __proto__ (隐式原型)也有 prototype (显式原型)
  2. __proto__prototype 都是一个对象,既然是对象,就表示他们也有一个 __proto__
  3. 实例对象 a 的隐式原型指向它构造函数的显式原型,指向的意思是恒等于
a.__proto__ === A.prototype

当调用某种方法或查找某种属性时,首先会在自身调用和查找,如果自身并没有该属性或方法,则会去它的 __proto__ 属性中调用查找,也就是它构造函数的 prototype 中调用查找。

ECMAScript 5 中引入了一个新方法 Object.create() 可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数。

var a = {a1};

var b = Object.create(a);

var c = Object.create(null);

什么是原型链

Object.prototype.b = ‘b’;
function Person() {};
var p = new Person();

p.a // undefined
p.b // ‘b’
  1. 实例对象 p 的隐式原型(__proto__)是一个对象,有两个属性值:constructor__proto__
  2. p.__proto__.constructor 返回的结果为构造函数 Person
  3. p.__proto__.__proto__.constructor 返回的结果为 Object() 函数

结合上面所讲的显式原型和隐式原型之间的关系,等同与

p.__proto__.__proto__ = Object.prototype

所以 p.b 打印结果为 bp 没有 b 属性,会一直通过 __proto__ 向上查找,最后当查找到 Object.prototype 时找到,最后打印出 b,向上查找的过程中,找不到 a 属性,所以结果为 undefined,这就是原型链,通过 __proto__ 向上进行查找,最终到 null 结束。

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。

另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty() 方法。

hasOwnProperty() 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。

注意:检查属性是否为 undefined 是不能够检查其是否存在的。该属性可能已存在,但其值恰好设置成 undefined

当你执行

var o = new Foo()

JavaScript 实际上执行的是

var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);

小结

查找属性,如果本身没有,则会去 __proto__ 中查找,也就是构造函数的显式原型中查找,如果构造函数的显式原型中也没有该属性,因为构造函数的显式原型也是对象,也有 __proto__,那么会去它的显式原型中查找,一直到 null,如果没有则返回 undefined

什么是原型继承

Person.prototype 只是一个指针,指向的是原型对象,但是这个原型对象并不特别,它也只是一个普通对象。假设说,这时候,我们让 Person.prototype 不再指向最初的原型对象,而是另一个类

function Person(){};
function Animal() {
    this.address = ‘abc’;
};
Person.prototype = new Animal();

var p = new Person();

p.address // ‘abc’

执行 Person.prototype = new Animal() 后,Personprototype 指针指向发生了变化,指向了一个 Animal 实例。

p 去访问 address 属性时,js 回先在 p 的实例属性中查找,发现找不到后,就会去 Person 的原型对象上查找。因为 Person 的原型对象已经被我们换成了 animal 实例,所以就会先找 animal 实例的属性。

这就说明,我们可以通过原型链的方式,实现 Person 继承 Animal 的所有属性和方法。

在使用原型继承编写复杂代码之前,理解原型继承模型是至关重要的。

此外,请注意代码中原型链的长度,并在必要时将其分解,以避免可能的性能问题。此外,原生原型不应该被扩展,除非它是为了与新的 JavaScript 特性兼容。


希望对读完本文的你有帮助、有启发,如果有不足之处,欢迎批评指正交流!

欢迎关注微信公众号【前端功成屋】分享一些前端技术、面试题、面试技巧

辛苦整理良久,还望手动点赞鼓励~


'摘抄'不是单纯的“粘贴->复制”,而是眼到,手到,心到的一字一句敲打下来。

声明:所有转载的文章、图片仅用于作者本人收藏学习目的,被要求或认为适当时,将标注署名与来源。若不愿某一作品被转用,请及时通知本站,本站将予以及时删除。