一、构造函数、原型
ES6前,是通过构造函数和原型来模拟类和对象的。 ES6前,创建对象可以通过以下三种方式:
- 利用new Object()创建对象:
var obj1 = new Object()
- 利用对象字面量:
var obj2 = {}
- 自定义构造函数
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)
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();
原型链的查找:
继承中,子类自身的构造器和一般方法,放在子类的原型对象上;但子类继承来的一般方法,放在其父类的原型对象上。
所以不管是子类的实例 还是父类的实例,调用的都是同一个方法。构造函数原型的存在,实现了所有创建的实例对象都使用同一个函数(节约了内存)
如果子类写了跟父类中同名的一般方法(即重写了父类的方法),在原型链往下查找时,找到了子类原型对象上的该方法后,就不会再继续往下找父类原型对象上的该方法。(就近原则)
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的指向)