7

前言

其他编程语言如 Java 等使用 new 命令时,都会调用“”的构造函数。但是,JavaScript没有“”,本身并不提供一个 class 实现(虽然在ES6中提供了class 关键字,但其只是语法糖,JavaScript仍然是基于原型的)。于是,JavaScript作了一个简化的思想,new 命令后面跟的不是类,而是构造函数,用构造函数生成实例对象,但其缺点是无法共享属性和方法。于是,就为构造函数设置了一个 prototype 属性,这个属性包含一个对象(prototype对象)。所有实例对象需要共享的属性和方法都放在这个对象里,那些不需要共享的属性和方法就放在构造函数里。

💡温馨提示:本文全文 1986 个字,推荐阅读时间为 10m,加油老铁!

一、显式原型(prototype)

1.1 介绍

每个函数在创建之后都会有一个名为prototype 属性:

function Parent() {

}
Parent.prototype.name = 'kite';
let child = new Parent();
console.log(child.name);

这个属性指向函数的原型对象(通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性),即调用构造函数创建的实例的原型,也就是例子中child的原型。

何为原型:每一个JavaScript对象(null 除外)都会和另一个对象相关联,这个对象就是原型,上面也提到每个对象都会从原型中“继承属性”。

原型表明了构造函数和实例原型的关系

1.2 作用

显式原型用来实现基于原型的继承与属性的共享。

二、隐式原型(__proto__)

2.1 介绍

JS中任意对象都具有一个内置属性[[prototype]],在ES5之前没有标准方法访问,大多数浏览器通过__proto__来访问。ES5中有了对这个内置属性标准的get方法:Object.getPrototypeOfObject.prototype 是个例外,它的__proto__null)。

function Parent() {

}
let child = new Parent();
console.log(child.__proto__ === Parent.prototype); // true

2.2 作用

隐式原型构成原型链,同样用于实现基于原型的继承。举例来说:
当我们访问对象中的某个属性时,如果在对象中找不到,就会一直沿着__proto__(原型的原型)依次查找,直到找到最顶层为止。

function Parent() {

}
Parent.prototype.name = 'dave';
let child = new Parent();
child.name = 'kite';
console.log(child.name); // 'kite'
delete child.name;
console.log(child.name); // 'dave'

上面的例子中,给对象child添加name属性,当访问name属性时,找到了对象本身的属性值kite。删除name属性之后,再次访问name属性,在对象中找不到该属性,再在原型中寻找,找到dave。假设属性在原型中也没有找到该属性,则会再去原型的原型中查找。

我们知道原型是个对象,既然是对象就可以用最原始的方式创建:

let obj = new Object();

原型就是通过Object 构造对象生成的。那 Object.prototype 的原型又是什么?

Object.prototype.__proto__ = null; // true

null表示没有对象,表明在此处可以停止查找了。

2.3 指向

__proto__ 指向创建这个对象的函数的显式原型,其关键在于找到创建这个对象的构造函数。创建对象有三种形式:对象字面量;newclass);ES5的Object.create()。本质只有一种new

2.3.1 对象字面量

let obj = {
    name: 'ctt'
}

对象字面量声明的对象继承自 Object ,和 new Object一样,其原型为
Object.prototype。而 Object.prototype 不继承任何属性和方法。

2.3.2 new

使用构造函数创建的对象,它的属性继承自构造函数。
具体分以下几种情况:

  1. 内建对象
    Array() ,它继承于Array.prototype Array.prototype 为一个对象,这个对象由 Object() 这个构建函数创建。因此,Array.prototype.__proto__ === Object.prototype,原型链为:Array.prototype -> Object.prototype -> null
  2. 自定义对象
  3. 默认情况下:

    function Foo() {};
    let foo = new Foo();
    Foo.prototype.__proto__ === Object.prototype;
  • 其他情况:

    // 想让Foo继承Bar
    function Bar() {
    }
    Foo.prototype = new Bar();
    Foo.prototype.__prototype__ = Bar.prototype;
    // 重新定义Foo.prototype
    Foo.prototype = {
      a: 1,
      b: 2
    };
    Foo.prototype.__proto__ = Object.prototype;

以上两种情况改写了Foo.prototype,所以Foo.prototype.constructor跟着改变,constructor和原构造函数 Foo切断了联系。

  1. 构造函数
    构造函数就是Function()的实例,因此构造函数的隐式原型指向
    Function.prototype 。引擎创建了Object.prototype ,而后又创建了
    Function.prototype,通过__proto__ 将两者联系起来。

Function.prototype === Function.__proto__ ,其他所有的构造函数都可以通过原型链找到 Function.prototype ,并且 function Function() 本质也是一个函数,为了不产生混乱,将 function Function__proto__ 联系到了Function.prototype 上。

2.3.3 class

在ES5中,每个对象都有一个 __proto__ 属性,指向对应构造函数的prototype 属性,而 class 作为构造函数的语法糖,同时具有 prototype 属性和 __proto__ 属性,所以同时存在两条继承链。

  • 子类的 __proto__ 表示构造函数的继承,总是指向父类;
  • 子类的 prototype 属性的 __proto__ 表示方法的继承,总是指向父类的
    prototype
class Parent {

}
class Child extends Parent {

}
Child.__proto__ === Parent;
Child.prototype.__proto__ === Parent.prototype;

作为一个对象,子类 Child 的原型(__proto__)是父类 Parent ;作为一个构造函数,子类的原型对象(prototype )是父类原型对象(prototype )的实例。

2.3.4 Object.create()

Object.create() 是ES5的方法,可以调用这个方法来创建一个新对象。新对象的原型就是传入的第一个参数。

三、构造函数(constructor)

上面说明构造函数和实例都可以指向原型,接下来讲的这个属性就是让原型指向构造函数的(没有指向实例的属性,因为构造函数可以生成多个实例),它就是constructor属性,每个原型都有一个constructor属性指向构造函数。

function Parent() {

}
console.log(Parent === Parent.prototype.constructor);

关于原型的各类关系总结如图:
6C8EFCFC-52BD-44DA-BE78-7F7D277DF9FA.png

四、this 和 prototype 定义方法的区别

  1. 利用this实现的方法,可以访问类中的私有变量和私有方法。而利用原型对象实现的方法,无法访问类中的私有变量和方法。
  2. 实例访问对象的属性或者方法时,将按照搜索原型链prototype chain的规则进行。首先查找自身的静态属性、方法,继而查找构造上下文的可访问属性、方法,最后查找构造的原型链。
  3. thisprototype定义的另一个不同点是在内存中占用空间不同。使用“this”关键字,实例初始化时为每个实例开辟构造方法所包含的所有属性、方法和所需空间,而使用prototype定义的,由于prototype实际上是指向父级的引用,因此在初始化和存储上比“this”节约资源。

五、总结

  1. 每个函数在创建之后都会有一个名为prototype属性,这个属性指向函数的原型对象(显式原型),所有实例对象需要共享的属性和方法都放在这个对象里;
  2. 任意对象都具有一个内置属性 __proto__ 指向创建这个对象的函数的显式原型;
  3. class 作为构造函数的语法糖,同时具有 prototype 属性和 __proto__ 属性,作为一个对象,子类 Child 的原型(__proto__)是父类 Parent ;作为一个构造函数,子类的原型对象(prototype )是父类原型对象(prototype )的实例。
  4. 每个原型都有一个constructor属性指向构造函数,即 Parent === Parent.prototype.constructor

参考

JavaScript - 原型、原型链 · Issue #13 · cttin/cttin.github.io · GitHub


云鱼
3.2k 声望528 粉丝

感谢阅读、浏览和关注!