js 你不知道的那些东西——js 中的原型

67 阅读5分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

js 中的原型

大多数指南/教程通过直接进入“构造函数”开始解释 JavaScript 对象,我认为这是一个错误,因为它们很早就引入了一个相当复杂的概念,使 Javascript 从一开始就显得困难和混乱。让我们把这个留到以后。首先让我们从原型的基础开始。

原型链(又名原型继承)

Javascript 中的每个对象都有一个原型。当消息到达对象时,JavaScript 将首先尝试在该对象中查找属性,如果找不到,则将消息发送到对象的原型,依此类推。这就像基于类的语言中的单亲继承一样。

prototypes-chain.png 原型继承链可以随心所欲。但总的来说,制作长链并不是一个好主意,因为您的代码可能难以理解和维护。

proto 对象

要理解 JavaScript 中的原型链,没有什么比 proto 属性更简单了。不幸的是 proto 不是 JavaScript 标准接口的一部分,至少在 ES6 之前是这样。所以你不应该在生产代码中使用它。但无论如何,它使解释原型变得容易

  //创建一个外来对象
    var alien = {
      kind: 'alien'
    }
    
  // 和person对象
    var person = {
      kind: 'person'
    }
    
  // 还有一个叫“zack”的东西
    var zack = {};
    
  // 指定alien作为zack的原型
    zack.__proto__ = alien
    
  //打印一下
   console.log(zack.kind); //输出=> ‘alien’
   
  // 指定person作为zack的原型
    zack.__proto__ = person
    
  // 再打印一下
   console.log(zack.kind); //输出=> ‘person’

如你所见, proto 属性非常易于理解和使用。即使我们不应该在生产代码中使用 proto ,我认为这些示例为理解 JavaScript 对象模型提供了最好的基础。

您可以通过以下方式检查一个对象是否是另一个对象的原型:

    console.log(alien.isPrototypeOf(zack))  //输出=> true

原型查找是动态的

您可以随时向对象的原型添加属性,原型链查找将按预期找到新属性:

   var person = {}
   
   var zack = {}
    zack.__proto__ = person
    
   // zack 此时对 kind 没有反应
    console.log(zack.kind); //输出=> undefined
    
   //但如果添加一下
    person.kind = 'person'
    
    
    //现在zack对kind做出了回应
    // 因为person能找到kind
    console.log(zack.kind); //输出=> 'person'

新的/更新的属性被分配给对象,而不是原型

如果更新原型中已经存在的属性会发生什么?让我们来看看:

    var person = {
      kind: 'person'
    }

    var zack = {}
    zack.__proto__ = person

    zack.kind = 'zack'

    console.log(zack.kind); //输出=> 'zack'
    // zack现在拥有“kind”的特性
    
    console.log(person.kind); //输出=> 'person'
    // person未被修改

请注意,属性“kind”现在同时存在于 person 和 zack 中。

对象.create

如前所述, proto 并不是一种将原型分配给对象的良好支持方式。所以下一个最简单的方法是使用Object.create() 。这在 ES5 中可用,但旧的浏览器/引擎可以使用这个es5-shim来填充。

    var person = {
      kind: 'person'
    }

    // person尚未被修改创建一个原型为person的新对象
    var zack = Object.create(person);

    console.log(zack.kind); //输出=> ‘person’

您可以将对象传递给 Object.create 为新对象添加特定属性

    var zack = Object.create(person, {age: {value: 13} });
    console.log(zack.age); //输出=> ‘13’

Object.getPrototype

您可以使用 Object.getPrototypeOf 获取对象的原型

    var zack = Object.create(person);
    Object.getPrototypeOf(zack); //输出=> person

没有 Object.setPrototype 这样的东西。

构造函数

构造函数是 JavaScript 中最常用的构造原型链的方式。构造函数的流行源于这样一个事实,即这是构造类型的唯一原始方式。许多引擎都针对构造函数进行了高度优化,这也是一个重要的考虑因素。

不幸的是,它们会让人感到困惑,在我看来,它们是新手发现 JavaScript 令人费解的主要原因之一,但它们是语言的重要组成部分,我们需要很好地理解它们。

    function Foo () { }

    var foo = new Foo();

    //foo现在是foo的一个实例
    console.log(foo instanceof Foo) //输出=> true

本质上,与关键字new一起使用的函数就像工厂一样,这意味着它们创建了新对象。他们创建的新对象通过其原型链接到函数,稍后会详细介绍。所以在 JavaScript 中我们称之为函数的实例

'this' 是隐式分配的

当我们使用“ new ”时,JavaScript 会以“ this ”关键字的形式注入对正在创建的新对象的隐式引用。它还在函数末尾隐式返回此引用。

当我们这样做时:

    function Foo () {
      this.kind = 'foo'
    }

    var foo = new Foo();
    foo.kind //输出=> 'foo'

在幕后,它就像在做这样的事情:

   function Foo () {
      var this = {}
      // this是无效的,仅供举例说明
      this.__proto__ = Foo.prototype;

      this.kind = 'foo'

      return this;
    }

但请记住,隐含的“ this ”仅在使用“ new ”时分配给新对象。如果您忘记了“ new ”关键字,那么“ this ”将是全局对象。当然忘记new是导致多个错误的原因,所以不要忘记new

我喜欢的一个约定是将函数的第一个字母大写,当它打算用作函数构造函数时,所以你现在直接发现你错过了new关键字。

“函数原型”

JavaScript 中的每个函数都有一个特殊的属性,称为“原型”。

    function Foo(){ }
    Foo.prototype

尽管听起来很混乱,但这个“原型”属性并不是函数的真正原型( proto )。

    Foo.__proto__ === Foo.prototype //输出=> false

这当然会产生很多混淆,因为人们使用术语“原型”来指代不同的事物。我认为一个很好的说明是始终将函数的特殊“原型”属性称为“函数原型”,而不仅仅是“原型”。

prototype ' 属性指向的对象将被指定为使用' new ' 时使用该函数创建的实例的原型。令人困惑?用一个例子更容易解释:

   function Person (name) {
      this.name = name;
    }

    //  person 具有原型属性
    // 我们可以向这个函数原型添加属性
    Person.prototype.kind = 'person'

    // 当我们使用new创建新对象时
    var zack = new Person('Zack');

    // 新物体的原型指向person。原型
    zack.__proto__ == Person.prototype //=> true

    // 在新对象中,我们可以访问亲自定义的属性。原型
    zack.kind //输出=> person

这几乎就是关于 JavaScript 对象模型的所有知识。了解 protofunction.prototype是如何相关的。