探索JS中的对象、构造函数与原型

148 阅读5分钟

引言

在JavaScript的世界里,对象、构造函数和原型是构建面向对象编程(OOP)基础的三大支柱。它们之间的关系既微妙又强大,共同支撑起了JS的面向对象体系。接下来,我们将通过一篇文章,详细探讨这三者的关系及其在JS中的应用。

一、对象字面量与ES6 Class

在JS中,创建对象的最直接方式是使用对象字面量。这种方法简单直观,但缺乏灵活性和可扩展性。例如:

javascript复制代码
	let person = {

	    name: 'Alice',

	    age: 25,

	    greet: function() {

	        console.log('Hello, my name is ' + this.name);

	    }

	};

然而,随着ES6的引入,class关键字为JS带来了更加结构化和可复用的对象创建方式。class可以看作是一个模板,用于批量制造具有相同属性和方法的对象实例。

javascript复制代码
	class Person {

	    constructor(name, age) {

	        this.name = name;

	        this.age = age;

	    }

	 

	    greet() {

	        console.log('Hello, my name is ' + this.name);

	    }

	}

	 

	let alice = new Person('Alice', 25);

尽管class在语法上更接近于传统面向对象语言中的类,但JS的class仍然是基于原型的。在底层,class只是语法糖,用于简化原型链的创建和继承。

二、构造函数与this指针

在JS中,构造函数是一种特殊的函数,用于创建和初始化对象。构造函数的名字通常以大写字母开头,以区别于普通函数。当使用new运算符调用构造函数时,会创建一个新的对象实例,并将this指针指向该实例。

javascript复制代码
	function Person(name, age) {

	    this.name = name;

	    this.age = age;

	}

	 

	let bob = new Person('Bob', 30);

在构造函数中,this指向的是新创建的对象实例。通过this,我们可以为实例添加属性和方法。

值得注意的是,函数是否是构造函数并不是由其名字的首字母大小写决定的,而是由调用方式决定的。即,只要使用new运算符调用,任何函数都可以作为构造函数。

三、原型与原型链

在JS中,每个函数对象都有一个prototype属性,这个属性指向一个对象,该对象被称为原型对象。原型对象包含了可以由所有实例共享的方法和属性。

当尝试访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JS引擎会沿着原型链向上查找,直到找到该属性或方法或到达原型链的末端(null)。

javascript复制代码
	function Person(name, age) {

	    this.name = name;

	    this.age = age;

	}

	 

	Person.prototype.greet = function() {

	    console.log('Hello, my name is ' + this.name);

	};

	 

	let charlie = new Person('Charlie', 35);

	charlie.greet(); // 输出: Hello, my name is Charlie

在这个例子中,greet方法被定义在Person的原型对象上,因此所有Person的实例都可以共享这个方法。

四、三者关系与实例对象

构造函数、原型对象和实例对象之间的关系可以概括为:

  • 构造函数:用于创建和初始化对象实例。
  • 原型对象:包含可以由所有实例共享的方法和属性。
  • 实例对象:通过构造函数创建的对象,具有自己的属性和方法(如果定义在构造函数内部),并且可以访问原型对象上的方法和属性。

这三者之间的关系是松散的,不像传统面向对象语言中的类那样紧密绑定在一起。这种设计使得JS的面向对象体系更加灵活和强大。

值得一提的是在JavaScript中,Person.prototype添加方法或属性的两种方式(直接赋值和逐个添加)在功能上通常是等效的,但它们在某些方面确实存在细微的差别,尤其是在处理原型链继承和现有实例的方法更新方面。

直接赋值给Person.prototype

javascript复制代码
	Person.prototype = {

	    eat: function() {

	        console.log(`${this.name}爱吃饭`);

	    }

	};

这种方式会完全替换Person构造函数原有的prototype对象。这意味着,如果之前已经在Person.prototype上添加了其他方法或属性,它们都会被新赋值的对象覆盖掉。此外,任何在替换之前已经创建的Person实例都不会继承新添加的方法,因为它们仍然引用着旧的prototype对象。

逐个添加方法到Person.prototype

javascript复制代码
	Person.prototype.eat = function() {

	    console.log(`${this.name}爱吃饭`);

	};

	 

	Person.prototype.greet = function() {

	    console.log('Hello, my name is ' + this.name);

	};

这种方式是在现有的Person.prototype对象上逐个添加新的方法或属性。这样做不会破坏现有的原型链,也不会影响已经创建的实例。新添加的方法会立即对所有现有的和未来的Person实例可用。

本质区别

  1. 原型链的连续性:直接赋值会破坏原型链的连续性,因为新的prototype对象与旧的没有直接联系。逐个添加方法则保持原型链的连续性。
  2. 现有实例的影响:直接赋值不会影响已经创建的实例对旧prototype对象上方法的引用,但这些实例无法访问新prototype对象上的方法。逐个添加方法则确保所有实例(无论新旧)都能访问新添加的方法。
  3. 代码的可维护性:逐个添加方法通常更易于理解和维护,因为你可以清楚地看到哪些方法被添加到了原型上,而不需要检查整个prototype对象是否被替换。

因此,除非你有充分的理由需要完全替换prototype对象(例如,在创建子类时重写父类的原型方法),否则通常建议逐个添加方法到prototype上。这样做可以保持原型链的完整性,确保所有实例都能正确地继承和使用新添加的方法。

五、总结

JS的面向对象体系是基于原型链的,这种设计哲学使得JS在面向对象编程方面既具有灵活性又具有强大的表达能力。通过构造函数、原型对象和实例对象的协同工作,我们可以创建出具有复杂结构和行为的对象系统。无论是使用对象字面量、构造函数还是ES6的class关键字,我们都可以根据具体需求选择最适合的方式来构建我们的对象系统。