JS的构造函数和原型链(从头到尾详细解释)

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() 时,发生以下步骤:

  1. 创建一个新对象。
  2. 将该对象的 [[Prototype]] 指向构造函数的 prototype
  3. 执行构造函数,将 this 绑定到新对象。
  4. 若构造函数未显式返回对象,则返回新对象。

原型链

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中,为什么不直接在构造函数上添加方法,而要把属性和方法放到它的原型对象上,这不是多此一举吗?

主要有以下几个原因:

  1. 节省内存空间

如果你将方法直接添加到构造函数的内部,每次创建一个新实例时,这个方法都会被复制一份。这样会导致在内存中存在多份相同的方法实例,浪费资源。

  1. 共享方法

当方法被定义在原型上时,所有实例共享这一个方法的引用。这样,所有 Person 实例只会有一个 greet 方法在内存中:

  1. 方便维护和扩展

将方法放在原型上使得代码的维护更加容易。如果需要修改或扩展一个方法,只需在原型上进行一次更改,所有实例都将自动获得这一更改。

  1. 利用原型链实现继承

使用原型的另一个好处是可以方便地实现继承。如果你希望某个子类继承父类的方法和属性,可以简单地让子类的原型指向父类的实例。这样,子类也可以访问父类通过原型定义的方法。

总结

将方法放到构造函数的原型对象上而不是实例上,主要是为了提高内存效率和代码可维护性,以及支持更灵活的继承机制。这种设计是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;');
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值