详解JS中的构造函数与原型

187 阅读6分钟

前言

在JavaScript中,我们有三种创建对象的方式,分别是:对象字面量,class关键字和function函数对象,本文将详细解析三者之间的关系特点以及优缺点。

对象字面量

对象字面量是创建对象最简单,直接的方式,只需要一个花括号{},它允许开发者直接定义和初始化一个对象,而不用通过构造函数或类来实例化。例如:

const Person = {
    name:奶龙
    age:18
}

这样一个Person对象就创建完成了,非常简单,但是它有一个很明显的缺点就是,你每每创建一个新的对象所有属性都要自己重新添加,随意且不灵活

class

class关键字是在es6时引入的,这使得 JavaScript 开发更加接近传统面向对象语言的概念。使用 class 关键字,你可以定义一个类,然后通过 new 关键字来创建这个类的实例。例如:

class person { 
    constructor(name,age){
        this.name = name
        this.age = age
    }
    sayHi(){
        console.log('hi');
    }
}
let ii = new person('大奶龙',18)

这就是通过传统的面向对象的思想来开发JS,通过类的封装将属性和方法做成一个模板,但是在es5时并不支持class,这时候就要请出我们的今天主角了。

构造函数

要理解何为构造函数?我们首先来看一串代码:

function Person(name,age){
    console.log(this);
    this.name = name;
    this.age = age;
}

如果这时我们在其下方传入参数调用:

Person('凯总',19)

这时这个函数就是以普通函数的方式运行,它的this指向的是window

但是如果我们要以构造函数的方式运行,便需要用new运算符创建一个实例对象出来,这时,this指针指向的是new出来的实例化对象,此时这个函数就是构造函数:

const k = new Person('yx',22); //this指向实例对象k

所以是不是一个构造函数是由其调用方式决定的,如果你需要的函数是一个构造函数,在命名时需要首字母大写,这并不是什么规则,而是一种编程风格,是一种约定俗成的格式。

那我们如果想要给对象增加一个方法该如何实现呢,当然我们可以直接在构造函数中添加方法,但是这并不是我们今天要说的,我们将用另一个种,也是JavaScript 面向对象编程的一个核心概念原型(prototype)来解决。

原型(prototype)

prototype是每一个函数创建时都拥有的一个属性,prototype属性是一个对象,即使你没有显示设置,JavaScript引擎也会自动为每个函数提供一个prototype对象。这个默认的 prototype 对象包含一个特殊的属性 constructor,该属性指向函数本身。此外,prototype 对象最初是空的,不包含任何其他属性或方法。

那么它是用来做什么的呢?

function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype = {
    eat:function(){
        console.log(`${this.name}爱吃饭`);
    }
}
const xx = new Person('xx',18)
xx.eat() // xx爱吃饭

你可以通过修改prototype来添加新的属性和方法,它可以让所有这个函数的实例化对象共享其中的属性和方法

const owen = {
    name:'owen',
    playBasketball:function (){
        console.log(`打篮球`);
    }
}
function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype = owen;
const kai = new Person('凯',18);
kai.playBasketball(); //打篮球

还可以将prototype的值等于一个对象,这样其他实例化对象也可以共享其中的方法。

任何一个实例都会有一个私有属性._proto_表达它构造函数的原型对象是谁,当一个对象调用一个属性或者方法时,它在自己内部没有找到,便会沿着这个私有属性去到原型对象中查找,这就是prototype的原理。

console.log(kai.__proto__ === owen);

image.png

当你没有在构造函数中添加this的指向也就是实例属性时(this.name=name)时,对象会自动去往原型对象中寻找:

function Person(name,age){
}
Person.prototype.name = '孔子'
let Person1 = new Person();
let Person2 = new Person();
console.log(Person1.name,Person2.name);

image.png

但是此时如果你打印一个console.log(Person1 === Person2);你会发现输出的值是False,这就是我们之前文章中讲到的,因为===会判断你的值和类型是否都相等,都相等才会返回True。在这里因为它们是两个不同的对象,在堆内存中所处的位置不同,即它们的引用(内存地址不同) 所以会输出False。

如果在构造函数中添加实例属性后,这些属性会被实例自身的属性覆盖掉,即便你没有给他添加值,它也只会返回undefined,并不会到原型对象中查找。

function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.name = '孔子'
Person.prototype.hometown = '山东'
let Person1 = new Person('x',18);
let Person2 = new Person('k',17);
console.log(Person1.name,Person2.name);

image.png

到这里为止差不多就已经结束啦,但是 那么总结一下:在es5的js中类的构建就是 = 构造函数(属性,对象的)+ 原型(方法,所有实例共享)

那这样做有什么好处呢?我们为什么方法不直接放在实例上,而是放在原型上呢?主要有两个原因:

  1. 性能优化:如果方法直接放在每个实例上,那么每次创建新的函数时都会创建一个新的执行上下文,这会占用更多的内存。而将方法放在原型上,所有实例都可以共享同一个方法,从而节省内存。
  2. 动态修改:通过原型,我们可以在运行时动态地添加、删除或修改方法,这对于某些需要高度灵活性的应用场景非常有用。

三者的关系

  • 构造函数:用来创建特定类型的对象。它定义了对象的基本结构(属性和方法)。
  • 原型对象:每个构造函数都有一个原型对象,原型对象包含了可以被该构造函数所有实例共享的属性和方法。
  • 实例:通过 new 关键字调用构造函数创建的对象。每个实例都会有一个内部链接指向其构造函数的原型对象,以便能够访问原型上的属性和方法。

小结

JavaScript 的面向对象编程提供了多种创建对象的方式,每种方式都有其独特的优势和适用场景。对象字面量适合创建简单的、一次性使用的对象;构造函数和原型机制提供了更强大的功能,适用于创建多个具有相同属性和方法的对象;ES6 的 class 关键字则使 JavaScript 的面向对象编程更加符合传统面向对象语言的编程习惯。

理解这些概念及其相互关系,有助于开发者更好地利用 JavaScript 的面向对象特性,编写出高效、可维护的代码。无论是前端开发还是后端开发,掌握这些基础知识都是必不可少的。

希望本文能帮助你更深入地了解 JavaScript 的面向对象编程,让你在开发过程中更加得心应手。