JS中的“暧昧”三角:构造函数、原型与实例对象的隐秘关联

385 阅读6分钟

引言:

今天刚拿到小黄书,就翻到了第二部分,别误会哦!我看的是你不知道的JavaScript。我发现了有一种特别的“三角关系”,它既简单又复杂,既直接又微妙。这三者分别是构造函数、原型对象和实例对象。本文将带你解密这暧昧的三角关系,以及它们如何共同构成了JavaScript面向对象的核心。

我们先来学学如何‘造对象’

对象字面量

let cao = {
    name: '张三',
};
let fan = {
    name: "王五",
    age: "18"
};

在JavaScript中,最简单的创建对象的方式就是使用对象字面量。这种方式直接、明了,但缺乏灵活性和复用性。那么我们可以思考一下怎么批量造!

ES6 Class:现代的面向对象


class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    eat() {
        console.log(this.name + '在吃饭');
    }
}

let lisi = new Person('李四', 18);
let agou = new Person('阿狗', 18);

随着ES6的推出,JavaScript引入了class关键字,使面向对象编程更加直观。class实际上是对构造函数和原型的一种语法糖封装,简化了代码的编写.那把我们的思绪回到推出ES6之前呢?

构造函数:面向对象的传统实现


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

const wen = new Person('雯', 18);
console.log(wen.age, wen.name);

在ES6之前的版本中,JavaScript通过构造函数来实现面向对象。构造函数首字母大写是一种编程习惯,帮助开发者区分普通函数与构造函数。构造函数通过new关键字创建实例对象。

对构造函数的理解

构造函数和普通函数如何区分?

  • 构造函数一般是要首字母大写的,但并不意味你大写了,它就是构造函数,这只是一个建议!便于区分于普通函数和构造函数的作用。其实普通函数和构造函数是一样的,重点是函数的调用方式
  • 该函数是否是构造函数,其实取决于你使用了new去调用,new是一个运算符。
  • 例如下面的示例中,Person('昌',19),没有使用new,这时就引了this关键字,只有你使用了new,this中的指针才会去指向实例化的对象,当你没有new对象时,指针不发生移动也就是指向window
  • 示例:
// 一定是构造函数吗? 不一定
function Person(name,age){
    console.log(this);
    this.name=name;
    this.age=age;  
}
Person('昌',19)// 普通函数运行 this 指向 window
const dys=new Person('昌',19);// 构造函数运行 this 指向 实例化的对象 dys
const dyf=new Person('威',20);//  

流程:

this 指针 指向 -> 实例对象 ->new 出来的对象

总结:

new就像老板,指示this去完成工作。有了new的指令,this指针才会指向实例化对象,完成属性的赋值,如此一来实例化对象也就生成了。

原型的引出

通过上面的内容,我们可以看到ES6中的语法,属性和方法都分装在了一起,一个类中,而之前的语法中,我还没有提及方法如何设计,这就到了第三者原型登场的了。

原型:共享的秘诀

原型是JavaScript面向对象的一个核心概念。每个函数都有一个prototype属性,指向一个对象,这个对象包含了所有实例共享的属性和方法。通过修改原型对象,我们可以让所有实例共享某些功能,从而提高性能。


function Person(name,age){
    console.log(this);
    this.name=name;
    this.age=age;  
}
// 每个函数都有一个原型对象
 Person.prototype={
    eat: function(){
        console.log(`${this.name}爱吃`);
    },
    sleep: function(){
        console.log(`${this.name}爱睡`);
    }

 }
 const K =new Person('KK',18);
 K.eat();
K.sleep();

解释:

简单来说,当你就函数实现了一个抽象类Person时,它就具有了.prototype原型对象,你可以在这里面添加方法, 如此之后, 你实例化的对象,它就业具有这个方法,并且可以使用。如上面的K.eat(); K.sleep();

原型式面向对象:独特的设计哲学

JavaScript的面向对象设计哲学与传统的类式继承不同,它基于原型。这种设计哲学类似于中国文化中以孔子为原型的思想传承,而不是基于血缘关系。通过将函数对象的prototype设置为另一个对象,可以实现方法的共享,这是JavaScript原型式面向对象的强大之处:


const cc={
    name:"cc",
    palyBasketball:function(){
        console.log(`${this.name}打篮球`);
    },
    palypingpang:function(){
        console.log(`${this.name}打乒乓`);
    }
}

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

// 原型? 
Person.prototype=cc;
const wu=new Person('武',20);
wu.palyBasketball();
wu.palypingpang();

//结果:
{}
武打篮球
武打乒乓
console.log(wu.__proto__==cc);
//结果:true

解释:

首先我们定义了cc对象,赋予了它name属性和两个方法,然后用函数构造了一个抽象对象Person,接着Person.prototype=cc;这一步很重要!

  • 这行代码的关键在于将 Person 构造函数的 prototype 属性设置为 cc 对象。这意味着所有通过 Person 构造函数创建的实例对象都会共享 cc 对象上的方法。这样实例化的wu,就能实现cc中的两个方法。

谈谈为什么可以调用原型上的方法?

重点在于这部分:

console.log(wu.__proto__==cc);
//结果:true
  • 原型链查找机制:当尝试访问 wu 实例对象上的 playBasketball 或 playPingPong 方法时,JavaScript 引擎会首先在 wu 对象本身查找这些方法。如果找不到,引擎会沿着 [[Prototype]] 链向上查找。
  • wu.__proto__ 指向 cc:由于 Person.prototype 被设置为 cc,所以 wu.__proto__ 就是 cc。因此,当在 wu 上查找 playBasketball 或 playPingPong 方法时,最终会在 cc 对象上找到这些方法。

‘暧昧’三角关系的奥秘

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

  • 构造函数通过new关键字创建实例对象this指向实例对象。
  • 实例对象通过内部的[[Prototype]]链接指向原型对象
  • 原型对象可以通过构造函数的prototype属性进行访问和修改。

图解

image.png

这种“三角关系”使得JavaScript能够在保持简洁的同时,实现强大的面向对象编程能力。每个实例对象都能访问到原型对象上的属性和方法,而原型对象则作为共享资源的中心。

对比于ES6中class的好处:

  • 动态修改原型:可以在运行时动态地修改原型对象,从而影响所有已存在的实例对象。例如,可以为原型对象添加新的方法或属性,所有实例对象都会立即获得这些新的特性。
  • 自由组合原型:可以将多个原型对象组合起来,形成复杂的继承关系。例如,可以将一个对象的原型设置为另一个对象,实现多继承的效果。
  • 直观的原型链:通过查看对象的__proto__属性,可以清晰地了解对象的继承关系,这对于调试和理解代码非常有帮助。

结语:

希望本文对你有所帮助,写的不好也请多多见谅。