Wang's blog Wang's blog
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Wang Mings

跟随大神,成为大神!
首页
  • 前端文章

    • HTML教程
    • CSS
    • JavaScript
  • 前端框架

    • Vue
    • React
    • VuePress
    • Electron
  • 后端技术

    • Npm
    • Node
    • TypeScript
  • 编程规范

    • 规范
  • 我的笔记
  • Git
  • GitHub
  • VSCode
  • Mac工具
  • 数据库
  • Google
  • 服务器
  • Python爬虫
  • 前端教程
更多
收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Python爬虫

  • 前端教程

    • 团队规范

    • Project

    • JS

      • Canvas基础
      • 数据结构
      • 树的深度优先遍历与广度优先遍历
      • for in和for of区别
      • ES6-新增特性一览
      • ES6-解构赋值及原理
      • ES6-Object
      • ES6-模块详解
      • ES6-Class
      • ES6-ECMAScript特性汇总
      • 输入URL背后的技术步骤
      • JavaScript与浏览器 - 线程与引擎
      • HTTP跨域解决方案
      • Http 2与Http 1.x比较
      • JavaScript原型
        • [#](#历史) 历史
        • [#](#原型) 原型
          • [#](#constructor) constructor
          • [#](#new) new
          • [#](#prototype) prototype
        • [#](#原型链) 原型链
        • [#](#总结) 总结
        • [#](#参考文章) 参考文章
      • JavaScript继承
      • JavaScript事件循环
      • 动手实现Promise
      • JS设计模式
      • JS 经典面试题
      • 排序算法
      • 正则表达式
      • MVC、MVP、MVVM区别
      • Array API与V8源码解析
      • 从V8 sort源码看插入排序
    • NodeJS

    • Vue

    • React

    • 效率工具

    • 读书笔记

  • 教程
  • 前端教程
  • JS
wangmings
2022-07-19
目录

JavaScript原型

# # JavaScript原型

JavaScript语言与传统的面向对象语言(如Java)有点不一样,js语言设计的简单灵活,没有class、namespace等相关概念,而是万物皆对象。虽然js不是一个纯正的面向对象语言,但依然可以对js面向对象编程。java语言面向对象编程的基础是类,而js语言面向对象编程的基础是原型。

原型是学习js的基础之一,由它衍生出许多像原型链、this指向、继承等问题。所以深入掌握js原型,才能对其衍生的问题有很好的理解。网上有很多文章解释原型里的等式关系,那样有些晦涩难懂,这里笔者从js设计历史来逐步解释js原型。

# # 历史

在ES6前,js语法是没有class的。倒不是js语言作者Brendan Eich忘记引入class语法,而是因为当初设计js语言时,只想解决表单验证等简单问题(估计js作者没想到后来js成为最流行的语言之一),没必要引入class这种重型武器,不然就跟Java这种基于class的面向对象语言一样了。具体可以看下阮一峰老师的Javascript继承机制的设计思想 (opens new window) (opens new window)。

虽然设计js语言时,更多的考虑轻量级灵活,但依然要在语言层面考虑对象封装以及多个对象之间复用的问题。先看下使用传统方式进行封装:

    function Person(name) {
        return {
          name: name,
          sleep: function() {
            console.log( 'go to sleep' )
          }
        }
    }
    
    var person1 = Person('tom')
    var person2 = Person('lucy')
    ... personN
    
1
2
3
4
5
6
7
8
9
10
11
12
13

传统方式有以下两个弊端:

  1. person1、person2...personN 实例对象没有内在联系,它们只是基于相同的工厂函数生成。
  2. 浪费内存,无法共享属性和方法。比如每个人的sleep方法是相同的,但生成10000个person实例会有10000个sleep方法占据内存

既然无意引入class语法,同时需要满足对象的封装以及复用问题,那就需要在js语言层面引入一种机制来处理class问题。

# # 原型

js作者使用了原型概念来解决class问题。那什么是原型?原型是如何在语法层实现的?会涉及到哪些概念?

# # constructor

js原型概念通俗讲有点像Java中的类概念,多个实例是基于共同的类型定义,就像tom、lucy这些真实的人(占据空间)基于Person概念(不占空间,只是定义)。java中类是基于class关键字的,但js中没有class关键字,有的是function。而java类定义中都有个构造函数,实例化对象时会执行该构造函数,所以js作者简化把构造函数constructor作为原型(代替class)的定义。同时规定构造函数需要满足以下条件:

  • 首字母大写
  • 内部使用this
  • 搭配使用new生成实例
    // java定义类
    class Person {
      // java类中都有构造函数
      constructor(name) {
        this.name = name
      }
    
      public void sleep() {
        ....
      }
    }
    
1
2
3
4
5
6
7
8
9
10
11
12
    // js使用构造函数代替类的作用
    function Person(name) {
      this.name = name
      this.sleep = function() { ... }
    }
    
1
2
3
4
5
6

# # new

以上通过构造函数定义了Person这个类。但如何区分一个function定义是构造函数还是普通函数?难道是看定义的函数里面是否有this来判断?

当然不是,js作者引入new关键字来解决该问题。因为构造函数只是定义原型(不占据内存),最终还是需要产生实例(占据内存)来处理流程,所以使用new关键字来产生实例。同时规定new后面跟的是构造函数,而不是普通函数。这样就区分出定义的function,是构造函数还是普通函数。

    // new 关键字后跟上构造函数,生成实例
    // 语法层面上也和Java实例类一致
    var tom = new Person('tom')
    var lucy = new Person('lucy')
    
1
2
3
4
5

你肯定注意到构造函数中this的疑问,它到底是在哪定义的?this又代表什么?其实在执行new的过程中,它会发生以下一些事:

  1. 新的对象tom被创建,占据内存。
  2. 把this指向tom实例,任何this上的引用最终都是指向tom
  3. 添加__proto__属性到tom实例上,并且把tom.__proto__指向Person.prototype(后续会讲的原型链)
  4. 执行构造函数,最终返回tom对象
    // 模拟new的实现
    function objectFactory() {
        var obj = new Object(),
        Constructor = [].shift.call(arguments); // 取出第一个参数,即构造函数
        obj.__proto__ = Constructor.prototype;
        var ret = Constructor.apply(obj, arguments);
        return typeof ret === 'object' ? ret : obj;
    };
    
    var tom = objectFactory(Person, 'tom')
    // 赋值的this === tom
    console.log(tom.name) // tom
    console.log(tom.sleep) // Function
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# # prototype

new 和 constructor 解决了模拟class类的概念,使得产生的多个实例对象有共同的原型,同类型对象内在有了一些联系。看上去很完美,但还有个问题:每个实例对象本质上还是拷贝了构造函数对象里的属性和方法。tom和lucy实例的sleep方法依然创建了两个内存空间进行存储,而不是一个。这样不仅无法数据共享,对内存的浪费也是极大的(想象下再生成10000个tom)。那js作者是如何解决这个问题的?

Brendan Eich为构造函数设置一个prototype属性来保存这些公用的方法或属性。prototype属性是一个对象,你可以扩展该对象,也可以覆写该对象。当你通过new constructor() 生成实例时,这些实例的公用方法(如:tom.sleep方法)并不会在内存中创建多份,而是通过指针都指向构造函数的prototype属性(如:Person.prototype)。

注意:Person构造函数和Person.prototype都是对象,拥有诸多属性。并且对象的属性依然可以是对象,万物皆对象核心。

    function Person(name) {
      this.name = name
    }
    
    // 构造函数都有一个非空的prototype对象
    // 可以扩展该对象,也可以覆写该对象,以下在原型上扩展sleep方法
    Person.prototype.sleep = function() { ... }
    
    var tom = new Person('tom')
    var lucy = new Person('lucy')
    tom.sleep === lucy.sleep // true
    
1
2
3
4
5
6
7
8
9
10
11
12

由于所有实例对象共享同一个prototype对象(构造函数的prototype属性),那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象一样。这就是我们通俗讲的:js面向对象编程是基于原型。

Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。

# # 原型链

我们再深入思考下,js是如何把各个实例跟构造函数的prototype对象(以下称原型对象)联系起来的?它们之间的通道是如何建立起来的?答案是使用new关键字。在上面我们模拟new关键字流程中,有个步骤是: 添加__proto__属性到tom实例上,并且把tom.__proto__指向Person.prototype。所以可以得到结论:实例与原型对象间关联起来是通过__proto__属性。

__proto__属性有什么用?当访问实例对象的属性或方法时,如果没有在实例上找到,js会顺着__proto__去查找它的原型,如果找到就返回。由于原型对象(如Person.prototype)也是一个对象,它也可以有自己的原型对象(比如覆写它),这样层层上溯,就形成了一个类似链表的结构,这就是原型链(prototype chain)。而通过覆写子类原型对象,再根据js原型链机制,可以让子类拥有父类的内容,就像继承一样,所以原型链是js继承的基础。

    tom.__proto__ === lucy.__proto__ === Person.prototype // true
    tom.sleep() // sleep方法是在原型链上找到的
    
1
2
3

注意new关键字以及原型链查找都是js语言内置的

# # 总结

  • 对原型的概念理解,语法层面不仅仅是prototype,还有constructor、new。
  • 可以把构造函数当作是特殊的函数,但记住它终归只是一个函数。
  • prototype属性是每个函数都有的,并且值是个不为空的对象,这在js语法层面就确定的。
  • __proto__属性是在实例对象上,prototype属性是在构造函数上,并且在new关键字作用下两者指向同一个地方。
  • js面向对象是利用原型来实现,js继承是利用原型链来实现的。

# # 参考文章

  • 阮一峰 - Javascript面向对象编程 (opens new window) (opens new window)

  • JavaScript深入之从原型到原型链 (opens new window) (opens new window)

  • Prototypes in JavaScript (opens new window) (opens new window)

  • JavaScript For Beginners: the ‘new’ operator (opens new window) (opens new window)

  • 汤姆大叔-JavaScript核心 (opens new window) (opens new window)

编辑 (opens new window)
Http 2与Http 1.x比较
JavaScript继承

← Http 2与Http 1.x比较 JavaScript继承→

最近更新
01
theme-vdoing-blog博客静态编译问题
09-16
02
搜索引擎
07-19
03
友情链接
07-19
更多文章>
Theme by Vdoing | Copyright © 2019-2022 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式