JavaScript|JavaScript 之原型、原型链

JavaScript|JavaScript 之原型、原型链
文章图片

前言 其他编程语言如 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);

关于原型的各类关系总结如图:
JavaScript|JavaScript 之原型、原型链
文章图片

四、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|JavaScript 之原型、原型链】JavaScript - 原型、原型链 · Issue #13 · cttin/cttin.github.io · GitHub

    推荐阅读