JavaScript ES5面向对象、原型、原型链详解

JS主打函数式编程,但其面向对象特性通过原型实现。类方法放在原型上避免多次拷贝,实现继承时通过原型链,如Student继承Person,确保构造函数调用和原型关联。原型链结构是理解JS对象查找和instanceof关键字工作原理的关键。

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

JS作为一个主打函数式编程范式的语言,对面向对象的实现相比其他语言有点特别。

在ES6引入class语法糖前,JS的类是这么写的:

// 构造函数
function Person(name) {
  // 类变量
  this.name = name;
}

// 类方法
Person.prototype.hi = function() {
  console.log('Hi, I am ' + this.name + '.');
}

const person = new Person('Sam');
person.hi(); // Hi, I am Sam.

类变量直接通过this.xxx申明在构造函数里,类方法则需要放在prototype也就是原型上。

为什么类方法要放在原型上?我直接跟类变量一样申明不香吗?比如:

function Person(name) {
  this.name = name;
  this.hi = function() {
    console.log('Hi, I am ' + this.name + '.');
  }
}

const person = new Person('Sam');
person.hi(); // Hi, I am Sam.

确实可以,但是这样会导致同一个类方法被拷贝多次,比如:

const person = new Person('Sam');
const anotherPerson = new Person('Mary');
console.log(person.hi === anotherPerson.hi);

上面这段代码,prototype写法下返回true,而直接申明在构造函数里的写法返回false,也就是说直接申明在构造函数里的写法会导致每次new都会将同一个方法拷贝一次,非常浪费内存。

所以类方法一定要定义在原型prototype上。

原型不仅可以防止方法重复拷贝,还是JS继承的实现方式。

定义一个Student类来继承之前的Person类:

function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}

Student.prototype = Object.create(Person.prototype, {
  constructor: {
    value: Student,
    enumerable: false,
    writable: true,
    configurable: true,
  },
});

Student.prototype.hello = function() {
  console.log('Hello, I am ' + this.name + ', a grade ' + this.grade + ' student.');
}

const student = new Student('Mary', 3);
student.hi(); // Hi, I am Mary.
student.hello(); // Hello, I am Mary, a grade 3 student.

关键的继承代码有两块,一个是Student构造函数内需要调用Person的构造函数来保证Person的类变量被Student继承:

Person.call(this, name);

另一个是令Student的原型为Person的空对象:

Student.prototype = Object.create(Person.prototype);

但是只靠上面的代码会导致Student.prototype.constructor === Person,所以我们还要把Student原型的constructor字段指向Student构造函数:

Student.prototype = Object.create(Person.prototype, {
  constructor: {
    value: Student,
    enumerable: false,
    writable: true,
    configurable: true,
  },
});

其中enumerable、writable、configurable分别代表getOwnPropertyDescriptor会返回的property descriptor中的可枚举(可以for...in或Object.keys())、可修改、可删除(delete字段)。

为什么Student的原型是Person的对象?为什么要把Student.prototype.constructor指向Student?

这就要说到原型链的结构。原型链其实就是爷类->父类->类->子类->孙类组成的一条类的继承链,在我们的例子中就是Object->Person->Student(Object是JS所有对象的默认父类),链的每一个环节由三个部分构成:构造函数(Person)、原型(Person.prototype)、对象(person)

三个指向:

  • 构造函数.prototype = 原型

  • 原型.constructor = 构造函数

  • 对象.__proto__ = 原型

这就是为什么上文代码中令Student.prototype.constructor = Student

两种初始化对象的方式:

  • 对象 = new 构造函数()

  • 对象 = Object.create(原型)

可以试着跑一下代码验证一下:

console.log(Person.prototype.constructor === Person); // true
console.log(person.__proto__ === Person.prototype); // true

原型链的每两个相邻环节之间,子类的原型是父类的对象,也就是说Person.prototype是Object的对象,Student.prototype是Person的对象。

可以试着跑一下代码验证一下:

console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true

把Object->Person->Student三个环节画在一张图里:

图中的Student.prototype--.__proto-->Person.prototype--.__proto-->Object.prototype--.__proto-->null这一条就被叫做原型链

这就是为什么上文代码中令Student.prototype为Person的空对象

除了继承,原型链还跟JS执行函数的机制相关,比如:

当执行student.hi()的时候,JS会按以下顺序寻找hi函数的定义:

  • 先在Object.getOwnPropertyNames(student)上找是否是构造函数中通过this.hi定义的

  • 再在Student.prototype上找Student.prototype.hi

  • 再在Student.prototype.__proto__也就是Person.prototype上找Person.prototype.hi(找到)

  • 再在Person.prototype.__proto__也就是Object.prototype上找Object.prototype.hi

  • 最后Object.prototype.__proto__为null,到达原型链终点,查找结束

instanceof关键字也是相似的原理,student instanceof Person之所以为true就是通过原型链一步一步找到了Person.prototype

差不多就写这么多~面试的话理解并记忆上面的原型链图就够了~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值