(JS基础)构造函数与原型、类与对象

77 阅读3分钟

一、构造函数、原型

ES6前,是通过构造函数和原型来模拟类和对象的。 ES6前,创建对象可以通过以下三种方式:

  1. 利用new Object()创建对象:var obj1 = new Object()
  2. 利用对象字面量:var obj2 = {}
  3. 自定义构造函数

1、构造函数

用于创建并初始化对象,即为对象成员变量赋初始值,它与 new 一起使用才有意义。

可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。

function Person(name, age) {
    //把接收到的实参赋给实例对象
    this.name = name;
    this.age = age;
    this.speak = function() {
        console.log(`${this.name},你好`);
    }
}
var p1 = new Person('tom', 18);
var p2 = new Person('jerry', 19);

语法:

1、该函数的首字母要大写

2、new一个实例对象的时候,会做的事情:

  • 创建一个空的对象
  • 把实参赋给该空对象的属性和方法
  • 返回该对象,用变量p1接收(所以构造函数里无需使用return语句)

3、this的指向问题:构造函数中的this 指向该类生成的实例对象

4、在构造函数中添加成员(即属性和方法):

  • 静态成员:在构造函数本身上添加的成员。只能由构造函数本身来访问

  • 实例成员:在构造函数内部创建的对象成员(在构造函数内部的 this 上添加的)。只能由实例化的对象来访问

//在构造函数本身上添加成员
Person.sex = '男'  
console.log(p1.sex)  //由实例来访问,是错误的

//undefined,不能通过构造函数来访问实例成员name、age和speak()
console.log(Person.name)  

5、浪费内存问题:

  • 构造函数中定义的方法speak是复杂数据类型,所以只要一创建实例对象,就会单独开辟一块内存空间用来存放函数。

  • 这样创建多个对象时,因为对象中方法的存在,会开辟多个内存空间,但它们存放的都是同一个函数。

  • console.log(p1.speak === p2.speak) //false 将存放方法的内存地址进行比较,不同的内存,地址不同。

希望所有创建的实例对象都使用同一个函数——>使用构造函数原型prototype

2、构造函数原型——prototype

JS 规定,每一个构造函数中,都有一个 prototype 属性,其值是一个对象(原型对象)。

在这个对象中写的属性和函数,即构造函数通过原型分配的属性和函数。在创建多个实例对象时,就不需要额外开辟多个空间。

console.dir(Person) //打印构造函数本身,展开后发现有prototype属性

当需要在对象中找方法的时候可以使用console.dir()进行打印,可以显示一个对象所有的属性和方法

原型

一个对象,也称为 prototype 原型对象。

作用:共享方法

把那些各实例都需要用到的函数,直接定义在 prototype 对象上,这样所有对象的实例可以共享这些函数,就不需要额外开辟多个空间。

每一个实例对象都会到原型对象里去找该方法

语法:

1、给构造函数的原型对象添加要共享的方法

2、不同的实例对象,使用这些共享的方法

Person.prototype.speak = function() {
    console.log('百老汇版的Six刷好几遍了,感恩世界');
}
p1.speak();
p2.speak();  //不会再开辟新的内存空间

console.log(p1.speak === p2.speak)  // true

实例对象可以使用构造函数原型对象(prototype)里的属性和方法,是因为该对象有 __proto__ 的存在。

对象原型 __proto__

打印实例对象:在控制台看到自动添加了该属性 __proto__,它指向原型对象prototype,展示的是原型对象上的方法。

原型对象上有某方法,实例对象p1就可以使用该方法。

console.log(p1)  

二、类、对象

在 ES6 中新增加了类的概念,用 class 关键字声明一个类,以这个类来实例化对象。

1、类与对象

语法:

1、 类名首字母大写

2、 constructor 构造器(构造方法),用来接收实参,把实参赋给具体生成的实例对象

  • constructor() 方法是类的默认方法,通过 new 命令生成对象实例时,会自动调用该方法。

  • 如果没有显示定义, 类的内部会自动创建一个constructor()

  • 类中的构造器不是必需的,需要对实例进行一些初始化的操作时才写。比如添加指定属性

3、类中的方法之间不能加逗号分隔,且不需要添加 function 关键字

4、this的指向问题:

构造器中的this,指向该类生成的实例对象。

一般方法中的this,指向调用了该方法的实例对象。(除非使用了call、bind这些调用

5、在类中可以直接写赋值语句(往实例身上追加了该属性及其值),不能使用let/const来定义变量(是在函数体里用的)。

函数体里可以定义变量,但在类中只可以写构造器、方法、赋值语句等等。

所以不需要传实参的有固定值的属性,可以直接写(无需在constructor里写this.属性 = 固定值

//1、创建类
class Person {
    //构造器
  constructor(name,age) {
      //this指向实例对象p1
      this.name = name;
      this.age = age;
    }
    
    //在类中可以直接写赋值语句。给Person的实例对象添加了a属性,值为1
    a = 1
    //给Person这个类本身添加属性
    // static b = 100
    
    //一般方法:除了构造器方法之外,自己根据业务需要写的
   speak() {  
      console.log(`${this.name},你好`);
   }
}

//2、创建实例对象
var p1 = new Person('tom', 18); 
console.log(p1.name)    
p1.speak()

//call可以改变this的指向,指向传入的对象
//p1.speak.call({ a:1,b:2 }) // undefined,你好

5、类中的构造器和一般方法,直接放在了类的原型对象上,供实例对象使用。在__proto__或者[[Prototype]]里可以看到:

console.log(p1)

1.jpg

2、继承

子类可以继承父类的属性和方法

super 关键字

在有继承行为的子类中使用。子类在调用对象父类上的函数(包括父类的构造函数和普通函数)时,用super()传参给父类。子类如果写了构造器,就必须调用super()

1、调用父类的构造函数

子类在构造函数中使用super, 必须放到 this 前面 (即,必须先调用父类的构造方法,再使用子类构造方法)

// 父类有加法方法
class Father {
    constructor(x, y) {
        this.x = x;  //this指的是父类创建的实例
        this.y = y;  //右边的y是形参,接收传递过来的参数,所以不要加this.
    }
    sum() {
        console.log(this.x + this.y);
    }
}

// 子类继承父类加法方法 同时 扩展减法方法
class Son extends Father {
    constructor(x, y) {
        // 利用super 调用父类的构造函数并传参
        // super 必须在子类的this之前调用
        super(x, y);
        this.x = x; //这里的this指的是子类创建的实例
        this.y = y;
//this.defalutValue = 27 给该属性写死值,无需传参进来
    }
    
    //减法
    subtract() {
        console.log(this.x - this.y);  //使用时要用this.  因为该方法里没有参数x,y  需要访问实例里的
    }
}

/*把实参传给子类的constructor,在子类通过super调用父类的构造函数时,把实参传给父类*/
var son = new Son(5, 3);

son.subtract();
son.sum();

原型链的查找:

继承中,子类自身的构造器和一般方法,放在子类的原型对象上;但子类继承来的一般方法,放在其父类的原型对象上。

所以不管是子类的实例 还是父类的实例,调用的都是同一个方法。构造函数原型的存在,实现了所有创建的实例对象都使用同一个函数(节约了内存)

2.png

如果子类写了跟父类中同名的一般方法(即重写了父类的方法),在原型链往下查找时,找到了子类原型对象上的该方法后,就不会再继续往下找父类原型对象上的该方法。(就近原则)

PS:当子类相对父类来说,没有需要扩展的属性或方法时,子类可以不用写构造器

class Son extends Father {

}

var son = new Son(5, 3); //8

2、调用父类的普通函数

class Father {
     say() {
         return '我是爸爸';
     }
}
class Son extends Father {
     say() {
          return super.say() + '的儿子';
     }
}
var damao = new Son();
console.log(damao.say());       

就近原则:继承中的属性或者方法查找原则

子类实例调用方法,先看定义子类时有没有该方法,有就执行子类的。

定义子类时没有的话,就调用父类中的。

3、注意点

先定义类,后实例化

在 ES6 中,类没有变量提升,所以必须先定义类,才能通过类实例化对象。

通过this调用属性、方法

类里面的共有属性和方法,要用this.访问

因为类中的属性和方法都是实例对象的,而this指向实例对象,否则会报undefined

类中this的指向

constructor 里的this指向实例对象, 一般方法里的this 指向这个方法的调用者(除非使用了call...改变了this的指向)