JavaScript的构造函数、原型对象和实例对象、原型链
前情引入:众所周知,在OOP(面相对象编程)中,对象是类的实例,类定义了对象的属性和方法。
结论:但在JavaScript中,没有类的概念(至少在ES6之前),而是通过构造函数(function)和原型链(prototype)**来实现面向对象编程。
我们可以类比 C++、JAVA中传统类 和 JavaScript构造函数来理解JavaScript中构造函数的概念。
基础回忆:实例对象由类创建。(实例对象:将类实例化为一个对象,这个对象就是实例对象。或者说通过定义好的类,在此基础上创建一个对象,这个对象就是实例对象。
面向对象编程的第一步,就是要编写对象,对象是单个实物的抽象,通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成,这个模板就是“”类。
那么问题来了:在javaScrpit中,并没有类的概念,那么javascript如何解决代码复用,对象继承的问题?
——总的用一句话回答就是:在ES5中通过“原型对象”(prototype)来实现,即基于原型(proto :原型,典型)的继承。
在ES6中,为了和大多数编程语言相近,也加入了类的继承(引入了class语法),但这只是语法糖。实际上还是通过原型对象来实现(即其底层依然是构造函数和原型链)。
在实际使用中,我们一般用class来区分普通函数和构造函数。class定义的只能作为构造函数,必须用new调用,而普通函数如果不用new,就是普通函数,但也可以被设计成构造函数。
例如,function Foo() {},如果通过new调用,就是构造函数,否则不是。而class Foo {},只能通过new调用,否则会报错。
在JS中,实例由构造函数通过new操作符创建。这里需要强调构造函数的作用类似于类,但又不同,因为JS是基于原型的。构造函数相当于“类”,但方法需附加到原型对象(prototype)上以实现共享。
构造函数(constructor):
-
定义:构造函数是一种特殊的函数,用于创建对象。(类似于 面相对象编程(OOP)里的类。)在JavaScript中,构造函数通常通过关键字
new来调用**。调用构造函数时,会创建一个新的对象,并将其绑定到this关键字。函数:在JavaScript中,函数是第一公民,这意味着函数可以像对象一样被传递和操作。函数本身也是对象,拥有属性和方法。构造函数也是一种特殊的函数(所以构造函数也是对象),它主要用于生成和初始化对象。
对象:对象是JavaScript的基本数据结构之一,用于**存储键值对。**构造函数可以用来创建对象实例,这些对象可以有自己的属性和方法。
比如说,定义一个构造函数Person
//示例代码:定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
-
构造函数与普通函数区别
- 普通函数的作用往往是执行某个操作或计算,而构造函数的目的是创建一个新的对象并初始化它(功能上)。
- 使用上: 构造函数使用New()来生成实例对象进行调用,普通函数直接调用
- 写法上:构造函数的首字母必须大写,用来区分于普通函数(驼峰命名),此为约定俗成。
当然,即使**构造函数的首字母是小写,您依然可以使用它像构造函数一样创建新对象。**JavaScript对函数本身并没有强制的命名规则,首字母大写只是一个约定俗成的惯例,用于提醒开发者这个函数是个构造函数。
- this的指向: 构造函数内部使用的this对象,来指向即将要生成的实例对象,而普通函数中的this指向调用函数的对象(没有对象时默认为window)
- 返回:构造函数默认return this,但也可以用return语句,返回值会根据return值的类型而有所不同。普通函数可使用return返回值
-
总结:构造函数通常是以大写字母开头的函数。在JavaScript中定义了一个构造函数之后,就可以用new关键字调用构造函数,生成一个实例(对象)。并且通过prototype属性向原型对象中添加方法,供实例对象共享。
-
在使用构造函数时,确保以下几点:
- 使用
new关键字调用构造函数,以确保this被正确绑定到新创建的对象上。 - 如果没有使用
new调用构造函数,this将指向全局对象(在非严格模式下),可能会导致意想不到的行为。
- 使用
原型对象:
定义:原型对象之所以叫做对象,是因为prototype属性是一个对象,所以叫原型对象。
网上一般描述为“构造函数有一个prototype属性指向原型对象”。但其实,构造函数的 prototype 属性就是原型对象本身。(而实例的 __proto__ 是指向构造函数的原型对象,二者要区分开)
function Person() {};//定义一个构造函数Person
console.log(Person.prototype); // 输出构造函数的原型对象prototype
原型对象中包含一些方法或属性。其存在的作用是共享方法和属性,避免每个实例都创建一遍相同的方法,节省内存。
- 反向引用:原型对象可以通过
constructor属性指回构造函数。 - 默认关系:因此,在未修改时,
Person.prototype.constructor === Person。
构造函数Person的原型对象可能会有一些方法,比如sayHello。可以通过构造函数的prototype属性在其原型对象上定义方法。
// 实例代码:
// 定义原型对象的方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}.`);
};
// 添加另一个原型方法
Person.prototype.getAge = function() {
return this.age;
};
构造函数和原型对象的关系:
构造函数有一个prototype属性指向原型对象,而原型对象有一个constructor属性指回构造函数。这样两者就相互关联了。
实例对象:
定义:用new关键字调用构造函数创建的实例,就是实例对象。**
const john = new Person('John', 30);
实例对象有一个内部属性 [[Prototype]](实际上是一个指针, 内部指针[[Prototype]])(可通过 __proto__ 或 Object.getPrototypeOf() 访问)连接到构造函数的原型对象。(在浏览器中可以通过______proto__访问)。所以当访问实例的属性或方法时,如果实例本身没有,就会去原型对象上找,如果还没有,就沿着原型链继续找,直到Object.prototype为止。
new 操作符的底层过程
当执行 new Constructor() 时,发生以下步骤:
- 创建一个新对象。
- 将该对象的
[[Prototype]]指向构造函数的prototype。 - 执行构造函数,将
this绑定到新对象。 - 若构造函数未显式返回对象,则返回新对象。
原型链:
JS规定,所有的对象都有自己的原型对象(即prototype属性)。函数也是对象,也不例外。函数实际是由构造函数Function实例化而来(不清楚或者感兴趣的可以见后文补充部分)。
①任何对象都可以充当其他对象的原型对象。
②原型对象也有自己的原型对象。
由此就会形成原型链(prototype chain)。
即**[[Prototype]] 链** ,即实例通过内部属性 [[Prototype]](可通过 __proto__ 或 Object.getPrototypeOf() 访问)连接到构造函数的原型对象的方法和属性,这使得它们可以共享功能。
const alice = new Person('alice',25);
console.log(alice.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
综上,整个代码块:
// 定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 定义原型对象的方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}.`);
};
// 添加另一个原型方法
Person.prototype.getAge = function() {
return this.age;
};
// 创建实例
const john = new Person('John', 30);
const jane = new Person('Jane', 25);
// 调用原型方法
john.sayHello(); // 输出: Hello, my name is John.
console.log(john.getAge()); // 输出: 30
jane.sayHello(); // 输出: Hello, my name is Jane.
console.log(jane.getAge()); // 输出: 25
问题来了:
在看完上述介绍之后,如果你能自然而然提出下面这个问题,那就说明你理解了上述的概念。
在JavaScript中,为什么不直接在构造函数上添加方法,而要把属性和方法放到它的原型对象上,这不是多此一举吗?
主要有以下几个原因:
- 节省内存空间
如果你将方法直接添加到构造函数的内部,每次创建一个新实例时,这个方法都会被复制一份。这样会导致在内存中存在多份相同的方法实例,浪费资源。
- 共享方法
当方法被定义在原型上时,所有实例共享这一个方法的引用。这样,所有 Person 实例只会有一个 greet 方法在内存中:
- 方便维护和扩展
将方法放在原型上使得代码的维护更加容易。如果需要修改或扩展一个方法,只需在原型上进行一次更改,所有实例都将自动获得这一更改。
- 利用原型链实现继承
使用原型的另一个好处是可以方便地实现继承。如果你希望某个子类继承父类的方法和属性,可以简单地让子类的原型指向父类的实例。这样,子类也可以访问父类通过原型定义的方法。
总结
将方法放到构造函数的原型对象上而不是实例上,主要是为了提高内存效率和代码可维护性,以及支持更灵活的继承机制。这种设计是JavaScript中面向对象编程的核心理念之一,使得代码更加模块化和清晰。
不过有时候容易混淆的是
构造函数的prototype属性和实例的__proto__属性。构造函数的prototype是显式的属性,而实例的__proto__是隐式的,实际指向同一个原型对象。
补充:函数的及其三种构造方式
函数的本质
在 JavaScript 中,**函数是 Function 构造函数的实例。**所有函数(包括构造函数)的 [[Prototype]] 都指向 Function.prototype。
function foo() {}
console.log(foo.__proto__ === Function.prototype); // true
函数声明的三种方式
-
函数声明(直接声明):
function add(a, b) { return a + b; } -
函数表达式(赋值给变量):
const add = function(a, b) { return a + b; };
通过 Function 构造函数(动态创建):
const add = new Function('a', 'b', 'return a + b;');
1306

被折叠的 条评论
为什么被折叠?



