构造函数?原型对象?实例?谁是谁是谁?????

216 阅读5分钟

引言

你是否见过这么一张让人绝望的原型关系图

2173695793-d09719e75573427e.webp

说到JavaScript的原型,这一直是一个非常重要,但又让人头疼的问题。别急,本文将带你一步步理解构造函数和原型对象之间讲不清,捋不清的微妙关系。

创建对象的方式

我们先来了解了解JS中的创建对象的各种方法

  • 对象字面量

    JS中最基础的创建对象的方法,对于一些简单的,少量的对象创建,不用想太多,直接用,好用!但看上去好像又没那么灵活,不适合创建复杂的对象。

    // 对象字面量
    let zhang = {
      name: '张三',
    }
    let li = {
      name: '李四',
      age: 17
    }
    
  • Class

    es6中新出现的关键字,可以定义,让JS更好的与传统的面向对象语言(C++,Java)相结合。它的封装性更好,代码可读性和可维护性更强。

    在这里引用《阮一峰 ECMAScript 6》中对class关键字的看法

    基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

     // class es6 
     // 构造函数+原型
     class Person {
      // 每个类只能有一个 constructor 构造函数
      // 也就是把下面要提到的构造函数放在了class之中
      // 比原型对象更加直观,但会受class的结构限制
       constructor(name, age) {
           this.name = name;
           this.age = age;
       }
       // 原型prototype部分
       eat() {
           console.log('吃饭');
       }
     }
     // 要使用new 
     let li = new Person('李四', 18)
     let wang = new Person('王五', 18)
    
  • 构造函数

    在es5还没有class的时候,我们使用构造函数来创建对象。使用new来实例化。是否是构造函数只需要看是否使用了new。为了区分构造函数与普通函数,我们一般约定把构造函数的首字母大写,但归根结底还是要看new。

    // 普通函数
    // function add(x, y) {
    //     console.log(this)  //全局global
    //     return x + y
    // }
    // console.log(add(1,2))
    
    // 和普通函数的区别在哪里?
    
    // 构造对象的过程  构造函数 constructor
    function Person(name, age) {
      console.log(this)    //Person{}
      this.name = name;
      this.age = age;
    }
    const zhang = new Person('张三', 18);
    console.log(zhang.name, zhang.age);
    const li = new Person('李四', 18);
    console.log(li.name, li.age);
    

构造函数和普通函数的区别:

  1. 语法和调用:构造函数需要使用new实例化,并且首字母大写。普通函数可以直接调用。
  2. this指向:构造函数中,this指向新创建的对象。普通函数在非严格模式下,this指向全局变量。严格模式下,this为undefined。
  3. 返回对象:构造函数中,如果构造函数没有返回值或返回一个非对象值(如字符串、数字、布尔值等),则新创建的实例对象会被隐式返回。普通函数可以返回任何类型的值。

原型(Prototype)

但凡是JS中的对象都拥有一个原型prototype属性。JS面向对象是原型式的面向对象(设计哲学)。

  1. 共享方法:使用同一个构造函数创建的对象可以共享原型对象上的方法。所以,我们通常把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

  2. 节约内存,动态管理:通过将方法定义在原型上,所有实例可以共享同一个方法,从而节省内存。可以在运行时动态地添加或修改方法,所有实例都会受到影响。

//原型对象prototype,  对象可以共享原型对象上的方法
// 构造函数 constructor 
function Person(name, age) {
    console.log(this);
    //将传入的name,age参数分别赋值给this对象的name,age属性
    //这样通过Person构造函数创建的对象将具有name属性
    this.name = name;
    this.age = age;
    
}
// 每个函数都有一个原型对象 在原型对象上添加方法
Person.prototype = {
    eat: function () {
        console.log(`${this.name}爱吃饭`);
    }
}
// 实例化
const zhang = new Person('张三', 18)
const li = new Person('李四', 18)
// 所有实例都能够调用这种方法
zhang.eat(); //张三爱吃饭
li.eat();  //李四爱吃饭

当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.name = '孔子'
Person.prototype.hometown = '山东'
let person1 = new Person('张', 18)
let person2 = new Person('李', 18)
// 创建的两个对象并不相同
console.log(person1 === person2);  //false
//先查找实例对象,找不到再去找原型对象
console.log(person1.name, person1.hometown, person2.name);
// 张 山东 李

JavaScript的面向对象设计哲学

JS的原型式面向对象不基于类的继承,而是基于原型的委托。相比较class而言,不会受到class的结构限制,更加灵活,允许动态修改行为。JS的面向对象更加符合前端开发的需求。

const lisi = {
    name: '李四',
    playBasketball: function () {
        console.log('科比来了');
    },
}
function Person(name, age) {
    console.log(this);
    this.name = name;
    this.age = age;
}
Person.prototype = lisi;
const san = new Person('张三', 19)
san.playBasketball() //科比来了

构造函数、原型对象与实例的关系

构造函数通过new来创建实例,在构造函数中总有一个原型对象,原型对象可以用来定义所有实例共享的属性和方法。实例通过原型链可以访问原型对象上的属性和方法。

用一张图来说就是

0ef1b028e91b23e0966eb17370c26e79.png

总结

在JavaScript中,构造函数、原型对象和实例是面向对象编程的核心概念。理解它们之间的关系对于编写高效、可维护的代码至关重要。希望这篇文章能帮助您更好地理解这些概念。如果您有任何其他问题或需要进一步的解释,请随时告诉我!