一、构造函数和原型
1、概述
在ES6之前,对象并不是基于类创建的,而是构造函数来定义对象及其特征。
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
而new在执行时会有四件事情:
- 在内存中创建一个新的空对象。
- 让 this 指向这个新的对象。
- 执行构造函数里面的代码,给这个新对象添加属性和方法。
- 返回这个新对象(所以构造函数里面不需要 return)。
静态成员及实例成员
- 静态成员:在构造函数本上添加的成员称为静态成员(利用对象.方法/属性),只能由构造函数本身来访问
- 实例成员:在构造函数内部创建的对象成员称为实例成员(利用this),只能由实例化的对象来访问
下面这个例子可以帮助你理解:
function Stra(uname, age) {
//构造函数里面的属性和方法我们叫做成员
this.uname = uname;
this.age = age;
this.sing = function() {
console.log("sing~~~");
}
}
var ldh = new Stra('ldh', 18);
//实例成员就是构造函数内部通过this添加的成员 sing uname age...
//实例成员只能通过实例化对象访问
console.log(ldh.age);
// console.log(Stra.age); //undefined
//静态成员 在构造函数本身添加成员
Stra.sex = 'male'; //这里就是静态成员
//只能通过构造函数访问
console.log(Stra.sex);
// console.log(ldh.sex); //undefined
2、构造函数存在的浪费内存问题及解决
我们先看一下这个例子:
function Star(uname, age) {
//构造函数里面的属性和方法我们叫做成员
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function() {
console.log("sing~~~");
}
var ldh = new Star('ldh', 18);
var zxy = new Star('zxy', 19);
// 一般公共属性定义到构造函数,方法定义到原型对象
console.log(ldh.sing === zxy.sing); //false
我们构造两个对象,判断一下两个对象的sing方法是否在同一内存,答案是false。这样就造成了内存资源的浪费。我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎样做呢?
这时候我们就要提出一个构造函数的属性—prototype(构造函数原型)。
构造函数通过原型分配的函数是所有对象所共享的。
JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。注意这个 prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法,就可以解决掉内存资源浪费的问题。
function Star(uname, age) {
//构造函数里面的属性和方法我们叫做成员
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function() {
console.log("sing~~~");
}
var ldh = new Star('ldh', 18);
var zxy = new Star('zxy', 19);
console.log(ldh.sing === zxy.sing); //true
这时候再判断,两个对象的sing方法指向就相同了。
下面我们再介绍一个概念:对象原型 __ proto__
每一个对象都会有一个__proto__属性,指向构造函数的prototype原型对象。
- __proto__对象原型和原型对象 prototype 是等价的
- __proto__对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype
是不是有些晕,看下面这张图,它能帮助你更直观的理解:
3、constructor构造函数及原型链
constructor构造函数
对象原型( __proto__)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。
constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
一般情况下,对象的方法都在构造函数的原型对象中设置。
我们通过输出一个构造函数的prototype属性,可以看到里面存在一个constructor属性,它是一个函数,指向该构造函数。
构造函数、实例、原型对象三者的关系
原型链
我们知道只要是一个对象就有__proto__属性,同时我们还知道一个构造函数的prototype指向一个对象,那么它的__proto__属性是什么呢,我们试着打印一下。
function Star(uname, age) {
this.name = uname;
this.age = age;
}
Star.prototype.sing = function() {
console.log("sing~~~");
}
var ldh = new Star('ldh', 18);
// 1、只要是对象就有__proto__属性,指向原型对象
console.log(Star.prototype.__proto__);
// Object.prototype里面的__proto__指向为null
它指向一个对象Object(),它会像套娃一样无止尽的指下去吗?答案是不会的,Object.prototype里面的__proto__指向为null。
这里有一张图,更加直观:
4、扩展内置对象
了解完了原型对象,接下来我们讲一个实用性的操作,利用原型对象扩展内置对象:
我们知道Array()对象里面有很多使用的方法,比如索引了、排序了之类的,但是我们相对数组求和的时候却发现没有这个方法,那么我们用原型对象来给Array()加一个sum求和方法吧。
我们先打印一下Array的原型对象:
里面有很多方法,但是没有我们想要的sum求和方法。
我们加一个试试:
// 添加array对象的求和功能
Array.prototype.sum = function() {
var sum = 0
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
}
// var arr = [1, 2, 3, 4];
console.log(Array.prototype);
可以看到Array()里面已经有了sum方法,能不能用呢?我们实践一下:
// 添加array对象的求和功能
Array.prototype.sum = function() {
var sum = 0
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
}
// var arr = [1, 2, 3, 4];
var arr = new Array(1, 2, 3);
console.log(arr.sum()); // 6
console.log(Array.prototype);
打印结果是6,看来可以准确地输出结果呢。
这里有一点需要注意
内置对象不能给原型对象覆盖操作 Array.prototype = {} ,只能是 Array.prototype.xxx = function(){} 的方式。
二、继承
ES6之前并没有给我们提供 extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
1、call()方法
es6之前是通过构造函数+原型对象实现继承的
call()可以调用这个函数并且修改函数运行时this的指向
方法:fun.call(thisArg, … ,);
实例:
function fun(x, y) {
console.log("drink water");
console.log(this);
console.log(x + y);
}
var o = {
name: "andy"
}
// 调用函数
// fun.call();
// 修改this的指向
fun.call(o, 1, 2); // 此时this指向o
这里我们在调用的时候,发现fun()中this的指向已经改变。
2、借用构造函数继承父类型属性
原理: 通过 call() 把父类型的 this 指向子类型的 this ,这样就可以实现子类型继承父类型的属性。
// 父构造函数
function Father(uname, age) {
this.name = uname;
this.age = age;
}
// 子构造函数
function Son(uname, age, score) {
Father.call(this, uname, age);
this.score = score;
}
var son = new Son('ldh', 18, 100);
console.log(son);
在子构造函数中,通过调用Father.call()方法可以更改Father的this指向,让其指向Son,这样就可以实现继承Father的属性了。那父亲的方法呢?我们一般使用原型对象。
3、利用原型对象继承父类型的方法
一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。
核心原理:
- 将子类所共享的方法提取出来,让子类的 prototype 原型对象 = new 父类()
- 本质:子类原型对象等于是实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象
- 将子类的 constructor 从新指向子类的构造函数
举个例子
父亲有挣钱的方法,儿子想从父亲那里继承该方法,并且还要有一个考试的方法。
先解决问题一:继承父亲的方法
我们可以使用Son的原型对象,指向父亲的实例化对象,这样Son的原型对象里面就有了earn()方法。
可以看到现在Son的prototype里面的prototype有了earn方法。
接下来是问题二:添加儿子的方法
直接在Son的原型对象里面添加exam方法
最后注意将Son原型对象的constructor函数指回Son。
// 父构造函数
function Father(uname, age) {
this.name = uname;
this.age = age;
}
Father.prototype.earn = function() {
console.log(10000);
}
// 子构造函数
function Son(uname, age, score) {
Father.call(this, uname, age);
this.score = score;
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.exam = function() {
console.log("考试");
};
var son = new Son('ldh', 18, 100);
console.log(son);
放个关系图,助于理解: