JavaScript原型指南:从零开始,解密构造函数的宝藏与对象的血统追踪

276 阅读5分钟

前言

JavaScript是一门灵活且富有表现力的脚本语言,其核心特性之一便是独特的原型(Prototype)机制。原型是JavaScript实现继承、对象属性共享和动态性的重要基石,今天我们就来拿下它。

函数(显示)原型:构造函数的魔法箱

原型是函数天生就具有的属性,它定义了构造函数制造出的对象的公共祖先,下面用一个例子来理解这个定义:

Person.prototype.say = function () {
  return 'Hello'
}

function Person() {
  this.name = 'hhh'
  this.age = 18
}

let p = new Person()
let p2 = new Person()


console.log(p2.say == p.say)  //ture

这里,Person.prototype上定义了say方法,所有通过Person构造函数创建的对象都能访问到这个方法,所以说p2.say才会和p.say相等。所以我们可以总结出,通过该构造函数产生的对象,可以隐式继承到原型上的属性和方法。

那么问题来了,我们的原型小姐姐到底有什么作用呢

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

// 在Person的原型上定义sayHello方法
Person.prototype.sayHello = function() {
    console.log("Hello, my name is " + this.name)
}

// 创建Person的实例
let person1 = new Person("yyy")
let person2 = new Person("hhh")

// 调用sayHello方法
person1.sayHello() // 输出: Hello, my name is yyy
person2.sayHello() // 输出: Hello, my name is hhh

在这个例子中,我们没有在每个Person实例中定义sayHello方法,而是将其添加到了Person构造函数的原型(Person.prototype)上。这样一来,所有通过Person构造函数创建的对象实例都可以访问到sayHello方法,不用为每个实例单独定义该方法,提取了公有属性,简化了代码的执行。

原型的增删改查

function Animal(name) {
  this.name = name
}

// 在Animal的原型上添加一个species属性
Animal.prototype.species = "Mammal"

let cat = new Animal("Kitty")

// 尝试修改实例cat通过原型链访问的species属性
cat.species = "Feline"

console.log(cat.species) // 输出: Feline,这是因为实例cat有了自己的species属性
console.log((new Animal("Dog")).species) // 输出: Mammal,原型上的species未改变

// 对于方法也是一样
Animal.prototype.sayName = function () {
  console.log(this.name)
}

// 实例cat尝试修改sayName方法
// 注意:实际操作中我们不会这么修改方法,这里仅为了演示
cat.sayName = function () {
  console.log("Meow, I'm " + this.name)
}

cat.sayName() // 输出: Meow, I'm Kitty,实例cat有了自己的sayName方法
let dog = new Animal("Dog")
dog.sayName() // 输出: Dog,原型上的sayName方法未改变

由此可见,实例对象是无法修改原型的属性和方法的,同理删除也是,那么我们以删除为例,对原型上的属性和方法做一些操作:

delete Animal.prototype.sayName

cat.sayName() // 如果成功删除,则此处会报错,因为sayName方法不存在了

需要注意的是,在实际开发中,直接修改内置对象的原型必须必须必须要谨慎,因为这可能会影响到代码的其他部分,导致程序无法正常运行。

对象(隐式)原型:家族遗传的宝盒

在介绍对象原型之前,我们先来看一个东西:

function Bus() {

}

function Car() {

}

var car = new Car()
console.log(car.constructor) // 输出:[Function: Car]

constructor纪录对象是由谁创建的

接下来重点开始了!
Person.prototype.name = '困困'
  function Person() {
  }
  var p = new Person()
  console.log(p)
  // 对象的原型是对象

name属性去哪了呢?我们在浏览器上可以清晰的看到: Snipaste_2024-05-13_19-39-53.png (这里的[[Prototype]]就是_proto_,是隐式原型的意思)

这就是为什么函数实例化得到的对象它没有显示继承到函数原型上的属性,但是它能访问函数原型上的属性,因为函数原型上的属性被对象继承了但是在对象的原型上,也就是说,实例对象会通过自己的隐式原型去继承构造函数显示原型上的属性和方法。

还是上面的例子,new到底做了什么事情?

function Person() {
    var this = {
      __proto__:Person.prototype
     }
  }

不难总结出:

  • 当访问对象属性时,先找对象显示具有的属性,没找到再去找对象的隐式原型

  • 实例对象的隐式原型 === 构造函数的显示原型

对象原型是对象继承特性的具体实现方式,它形成了对象之间的原型链

原型链:家族血脉的传承之路

顺着对象的隐式原型不断地向上查找上一级的隐式原型,直到找到目标或者一直到null,这种查找关系叫做原型链

Person.prototype = {
    name: '桃桃',
    sayName: function () {
      console.log(this.name)
    }
  }
  function Person() {
    this.name = '困困'
  }
  var p = new Person()
  p.sayName() 

思考一下,最后打印的是桃桃还是困困?也就是看this代表的是什么喽,如果this指向的是对象,那么就是桃桃,如果this代表构造函数,那就会拿到困困

Snipaste_2024-05-13_20-06-05.png 嘿嘿嘿,是不是和你想象的不一样呢,事实上,构造函数原型里的this,指向的也是实例对象

来看一张神图帮助理解原型链

v2-c7ad4ebf871a5bdaa836951daca0d05b_r.png

最后有一个小问题,所有的对象都会继承自Object.prototyp吗?

谜底揭晓!

几乎所有的JavaScript对象都会直接或间接地继承自Object.prototype,但是!!!有特例,原因在于JS判定数据类型是靠将变量转换为二进制数据来实现的,如果前三位是000就判定它是引用数据类型,但是null很特殊,虽然它是原始数据类型,但是它二进制转换后通体都是0,自然也被读成了引用数据类型,所以唯独这个null是没有隐式原型的对象。

尾声

今天的知识就分享到这里啦,祝大家早日上岸!