javascript知识梳理(二):数据结构-原型(prototype)

本文深入探讨JavaScript中的原型和原型链,解释了如何通过原型实现类的继承,以及如何扩展内置对象的方法。文章详细阐述了`this`和`new`关键字在继承中的作用,并提供了示例代码,展示了如何利用原型链实现类的继承和数组方法的拓展。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

本篇文章的目的是尽可能的解释清楚在js运行时的原型链。我准备从三个问题着手:

  1. 原型是什么?
  2. 原型链是什么?
  3. 原型链的应用?

二、什么是原型?

  • 在编写javascript代码过程中,经常会用到ArrayObjectFunction等构造器创建对象。如下:
      const obj1 = {}
      const obj2 = new Object()
      console.log(obj1.toString) // [object Object]
      console.log(obj2.toString) // [object Object]
    
  • 如上,在创建obj1时,我们并没有给它添加toString方法。但是它却能够使用toString方法。这是为什么呢?这是因为toString方法是在Object的原型上,而在创建obj1时,会将Object的原型挂载到obj1的原型链上。Object的原型是什么呢,可以通过浏览器控制台console.log(Object.prototype)打印出来。如下:
      console.log(Object.prototype)
      console.log(typeof Object.prototype) // object
      console.log(Object.prototype === obj1.__proto__) // true
      // constructor: ƒ Object()
      // hasOwnProperty: ƒ hasOwnProperty()
      // toLocaleString: ƒ toLocaleString()
      // toString: ƒ toString()
      // ....
    
  • 每一个构造方法都有一个prototype属性,它构造的每一个对象,都会有一个__proto__属性,这个**__proto__属性就是原型**。而且原型也只是一个普通的对象。在javascript中,每一个对象,都有自己的原型。万物皆对象,指的就是所有的数据类型,都有一个共同的原型,Object.prototype。所以他们都是Object构造方法创建的。

三、什么是原型链

  • 通过上面的例子,可以知道,所有的对象,都有一个共同的原型,那就是Object.prototype。但是,Object.prototype的方法却不多,而在实际开发中,有很多方法都是Object.prototype中没有,本身也没声明,但是却可以用的,比如:

      const arr = []
      arr.splice(0, 1, 1)
      console.log(arr) // [1]
      console.log(arr.toString) // 1
    
  • 在执行上述代码中,arr对象不仅仅调用了Object.prototype原型中的方法,还调用了一个splice方法,这个在原型跟创建对象时都没有声明的,但他却执行了,这是为什么呢?尝试找一找原因:

      console.log(arr.__proto__)
      // concat: ƒ concat()
      // constructor: ƒ Array()
      // copyWithin: ƒ copyWithin() 
      // splice: ƒ splice() 
      // ....
      console.log(arr.__proto__.__proto__ === Object.prototype) // true
      console.log(arr) // [1]
      console.log(arr.toString) // 1
    
  • 通过上述代码的执行,可以发现,原型并不只是单独的某一个对象,它可以是多个对象一级一级通过__proto__属性串联起来,形成一个链式的结构,而splice方法,正是在此条链上可以找得到,这就是js中的原型链结构。js在获取对象属性时,会先查找当前对象有没有该属性,如果无,则查找__proto__指向的对象是否有,在__proto__对象上继续此操作,直到找到该属性或者__proto__null为止。而在此例中,toString方法是在arr.__proto__.__proto__中找到,而splice则是在arr.__proto__找到。

  • 原型链查找流程图

原型链查找流程图

四、原型链的应用

1、实现类与类的继承

javascript原本是基于原型的语言,原本是没有类的概念,但是由于一些历史原因,在初期为了推广,仿照java语言,在基于原型的基础上,又引入了thisnew等语言特性,能通过一些妥协基本实现类的功能。

this

this关键字,是在运行时全局作用域与函数作用域下可以直接访问的对象。

  • 全局作用域下(浏览器环境)window
  • 方法调用: 执行该函数时调用的对象。
  • 普通函数执行window
  • 严格模式非方法调用undefined
  • 如浏览器控制台执行如下代码:
      console.log(this) // window
      const obj = {
        print: function() {console.log(this)},
        strictPrint: function() {
            "use strict"
            console.log(this)
          }
      }
      const print = obj.print
      const strictPrint = obj.strictPrint
      print() // window
      obj.print() // obj
      strictPrint() // undefined
      obj.strictPrint() // obj
    
  • 通过打印可以看出,this的初始化在运行时创建作用域时完成,按照一定规则赋值的一个对象。所以this也会有自己的原型链,而js中的类,可以利用this的原型链特性,实现类的继承。
new

new关键字,一般用来创建类的实例,但是在js中,并没有类的概念。但是new,还是通过一些转变,实现了类似的功能。如下:

  function Dog(name) {
    this.name = name
    this.say = function() {console.log(this.name + ':汪,汪,汪~'+this.type)}
  }
  const dog1 = new Dog('小黑')
  const dog2 = new Dog('大黄')
  dog1.say() // 小黑:汪,汪,汪~undefined
  dog2.say() // 大黄:汪,汪,汪~undefined
  function Husky(name) {
    this.name = name,
    this.type = 'Husky'
  }
  Husky.prototype = new Dog() // 绑定原型链
  Husky.prototype.constructor = Husky // 还原构造函数
  const husky1 = new Husky('小黑')
  const husky2 = new Husky('大黄')
  husky1.say() // 小黑:汪,汪,汪~Husky
  husky2.say() // 大黄:汪,汪,汪~Husky
  • 上述代码实现了一个简单的类以及类的继承。

    但是,要注意的是,基于原型的继承。在运行时如果改变了原型链上的方法。那么原型链的所有分支都将收到影响。因为原型链仅仅是模仿了继承,如果要实现更高级的继承。那么需要额外引入一些方法。比如使用方法对象的call,apply等方法,将继承对象的属性挂载到当前对象上。

  • 上述代码中,new实际是js帮忙封装了几步。new的实际操作用代码拆分如下
      function Dog(name) {
        this.name = name
        this.say = function() {console.log(this.name + ':汪,汪,汪~'+this.type)}
      }
      function newDog(name) {
        // 创建一个新对象
        const obj = {}
        // 挂载原型链
        obj.__proto__ = Dog.prototype
        // 利用Dog方法对象原型链上的call方法,重新指定执行是this对象为obj
        Dog.call(obj, name)
        return obj
      }
      const dog = newDog('大黄')
      dog.say() // 大黄:汪,汪,汪~undefined
    
  • 在开发中,如果偏爱使用类进行编码的,可以通过以上方式,利用原型链实现类似的功能

2、拓展内置对象

Array数组的方法
  • 如果有一个需求,需要在运行时,在多个模块或地方要对数组对象进行定制化操作,有什么方法可以快速实现这个需求呢?
  • 有的,比如,假设需要对数组中的每一个number类型的项进行一次自增,因为所有的数组实例,都有共同的原型为Array.prototype。所以可以将此方法挂载到Array.prototype上,就实现了所有数组实例都能直接调用该方法了。如:
  Array.prototype.add = function() {
    this.forEach((item, index) => {
      if(typeof item === 'number') this[index] = ++item
    })
    return this
  }
  const arr = [1,2,'aaa']
  console.log(arr.add()) // [2, 3, "aaa"]
  • 当然,也可以重写数组的内置方法,vue中就是这么实现数组的数据侦听的。

以上就是本人对原型链的理解跟总结了,如有不足不实之处,欢迎各位大佬批评指点~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值