构造函数
说到原型和原型链就离不开构造函数,构造函数就是用来创建对象的方法。
构造方法
这里的People
方法我们就称为构造方法。
// 构造函数
function People(name){
this.name = name
}
// 实例化对象
const p1 = new People('randy');
new一个新对象的过程,发生了什么?
- 在内存中创建一个新对象。
- 这个新对象内部的
[[Prototype]]
指针被赋值为构造函数的prototype
属性。 - 构造函数内部的
this
被赋值为这个新对象(即this
指向新对象)。 - 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
代码实现
function _new(fun, ...args) {
// 创建一个对象,并以构造函数的prototype为原型
const obj = Object.create(fun.prototype为原型)
// 将该对象作为this传递进构造函数,然后运行构造函数进行赋值
const result = fn.apply(obj, args)
// 如果构造函数有返回值,并且是对象,则返回该对象,否则为我们创建的对象
return result instanceof Object ? result : obj
}
// 使用的例子:
function People(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
// 如果 返回非空对象,返回的就是该对象
// return { name: "demi" };
}
const p1 = _new(People, 'randy', 24)
console.log(p1) // People {name: 'randy', age: 24, sayName: ƒ}
变量
变量分为静态变量和成员变量。
- 静态变量:在构造函数上添加的成员,只能通过构造函数来访问。
- 成员变量:成员变量是定义在构造函数中。这种变量在创建对象的时候实例化,只能通过实例对象访问。每个实例对象私有,改动互不影响。
// 构造函数
function People(sex){
// 成员属性
this.sex = sex
this.say = function() {
console.log(this.sex)
}
}
// 静态属性
People.age= 24
// 实例化对象
const p1 = new People('male');
const p2 = new People('female');
console.log(p1.sex); // male
console.log(p1.age); // undefined 获取不到,静态属性只能通过构造函数获取
console.log(p1.say()); // male
console.log(People.sex); // undefined 获取不到,成员属性只能通过实例对象获取
// console.log(People.say()); // 报错 People.say is not a function
console.log(People.age); // 24 静态属性能直接通过构造函数获取
console.log(p2.sex); // female
console.log(p2.age); // undefined 获取不到,静态属性只能通过构造函数获取
console.log(p2.say()); // female
// 每个实例有自己的存储空间
console.log(p1.say == p2.say); // false
原型
从上面的例子我们可以看出,成员变量会存在在每个成员上面并且相互独立,所以就算是相同得属性(比如say方法),会同时存在在多个实例对象内存空间上,大大浪费了存储空间。
接下来我们就用原型来解决这个问题。
在解决之前,我们先来看看什么是原型?以及原型和构造函数的关系。
什么是原型?
当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针__proto__
,这个指针指向构造函数的 prototype
属性对应的值,在 ES5
中这个指针被称为对象的原型。
所以prototype
的值是一个对象,包含两个属性,一个是constructor
,一个是__proto__
,在控制台__proto__
其实是以[[Prototype]]
显示的。
上面的例子People.prototype
或 p1.__proto__
就是原型,它是一个对象,我们也称它为原型对象。
构造函数会有prototype
和__proto__
,但是实例对象只有__proto__
。
原型和构造函数的关系
每个构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针__proto__
。
所以在这里我们可以得到
console.log(People.prototype.constructor === People) // true
console.log(People.prototype === p1.__proto__) // true
所以我们使用原型来优化下上面的例子。
// 构造函数
function People(sex){
// 成员属性
this.sex = sex
}
// 把方法放到原型上共享
People.prototype.say = function() {
console.log(this.sex)
}
// 实例化对象
const p1 = new People('male');
const p2 = new People('female');
console.log(p1.sex); // male
console.log(p1.say()); // male
console.log(p2.sex); // female
console.log(p2.say()); // female
// 原型上的方法是相同的
console.log(p1.say == p2.say); // true
从上面的例子我们可以看出,把方法写到原型上每个实例对象也都能各自访问并且只占据一份内存空间。是不是就大大减少了内存空间的消耗呢。
原型特点
- 所有的构造函数都有
prototype
和__proto__
属性。 - 实例对象没有
prototype
属性,只有__proto__
属性。 - 实例对象的
__proto__
属性等于其构造函数的prototype
属性。
如何获取一个对象的原型?
假设p
是一个对象
p.__proto__
p.constructor.prototype
Object.getPrototypeOf(p)
Reflect.getPrototypeOf(p)
如何判断一个对象是否是另外一个对象的原型?
obj1.isPrototypeOd(obj2)
如何设置一个对象的原型呢?
Object.setPrototypeOf(obj, proto)
Reflect.setPrototypeOf(obj, proto)
原型链
上面的例子中,我们实例对象里面明明没有say
方法,为什么却能访问到呢?这就涉及到我们的原型链知识啦。
什么是原型链?
原型与原型层层相链接的过程即为原型链。
所以上面没报错能调用say
方法就是用到了原型链。下面说说原型链的查找。
- 首先看p1对象身上是否有say方法,如果有,则执行p1对象身上的方法。
- 如果没有say方法,就去构造函数原型对象prototype身上去查找say这个方法。
- 如果再没有say方法,就去Object原型对象prototype身上去查找say这个方法。
- 如果再没有,则会报错。
// 实例对象的原型等于其构造函数的原型对象
p1.__proto__ === People.prototype;
// 由于原型对象是对象所以其构造函数是Object
p1.__proto__.__proto__ === People.prototype.__proto__ === Object.prototype;
// 原型的尽头就是null
p1.__proto__.__proto__.__proto__ === People.prototype.__proto__.__proto__ === Object.prototype.__proto__ === null;
函数
函数的原型又是怎么样的呢,又有什么特点呢?我们再把上面的例子改一下。
// 使用new Function创建构造函数
const People = new Function("sex", "this.sex = sex");
const p1 = new People('male');
console.log(p1); // {sex: 'male'}
所以我们还可以得到
// 实例对象的原型等于其构造函数的原型对象
People.__proto__ === Function.prototype;
// 因此还可以得到
Object.__proto__ === Function.prototype;
Function.__proto__ === Function.prototype;
// 由于原型对象是对象所以其构造函数是Object
Function.prototype.__proto__ === Object.prototype;
// 原型的尽头
Object.prototype.__proto__ === null
下面用网上流传的一张图来总结
讲到这,小伙伴们是不是对原型和原型链有了进一步的了解呢?如果还有不懂,欢迎评论区留言探讨。
类 class
类的本质还是一个函数,类就是构造函数的另一种写法。
function People(){}
console.log(typeof People); //function
class People {}
console.log(typeof People); //function
类的特点
- 定义于
constructor
内的属性和方法,即定义在this
上,属于实例属性和方法。定义于class
内的属性属于实例属性。定义于class
内的方法会被定义在原型上。
class People {
// 定义于 class 内的属性属于实例属性
_age = 24;
// 静态属性
static num = 100;
constructor(name) {
// 定义于 constructor 内的属性和方法,即定义在 this 上,属于实例属性和方法。
this.name = name;
this.say = function () {
console.log("say");
};
}
// get方法
get age() {
return this._age;
}
// set方法
set age(newAge) {
this._age = newAge;
}
// 定义于 class 内的方法会被定义在原型上
sing() {
return this.name;
}
// 静态方法
static hello() {
console.log("static hello");
}
}
let p1 = new People("randy");
let p2 = new People("demi");
console.log(p1);
console.log(p1.hasOwnProperty("name")); //true
console.log(p1.hasOwnProperty("age")); //false
console.log(p1.hasOwnProperty("sing")); //false
p1._age = 25; // 能直接改
p1.age = 26; // 有set方法才能修改age的值
console.log(p1._age); // 26
console.log(p1.age); // 26
// 静态方法和属性只能通过class来访问
People.hello(); // static hello
console.log(People.num); // 100
我们来看看p1
- 一个类必须有
constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。 - 函数声明会提升,类声明不会。首先需要声明你的类,然后才可以创建对象,否则会报错。
class
内部默认使用的是严格模式。
类和构造函数的区别
- 类必须使用
new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行,只不过是当做方式使用。 - 类的所有实例共享一个原型对象。
- 类的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
- 新的class写法,其实就是语法糖,只是让对象原型的写法更加清晰,更像面向对象编程的语法而已。
说完原型和原型链接下来我们来说说JS继承。
JS继承
JavaScript
中没有类的概念的,主要通过原型链来实现继承。通常情况下,继承意味着复制操作,然而 JavaScript
默认并不会复制对象的属性,相反,JavaScript
只是在两个对象之间创建一个关联(原型对象指针),这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承。
原型继承
原型链继承主要是利用原型对所有实例共享的特性来实现继承的。该继承方式主要有如下特点
- 原型在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。
- 创建子类型的时候不能向超类型传递参数。
- 实例属于子类不属于父类。
下面我用例子说明
// 子类
function Child(name, age) {
this.name = name;
this.age = age;
this.colors = ["red", "blue"];
this.say = function () {
console.log("child say", this.name, this.age);
};
}
//原型对象
const Father = {
sayFather() {
console.log("child sayFather", this.name, this.age);
},
fatherColors: ["green", "yellow"],
};
// 原型继承
Child.prototype = Father;
// c1
let c1 = new Child("randy", 24);
console.log(c1);
c1.say(); // child say randy 24
c1.sayFather(); // child sayFather randy 24
console.log(c1.colors); // ['red', 'blue']
console.log(c1.fatherColors); // ['green', 'yellow']
// c2
let c2 = new Child("demi", 25);
console.log(c2);
c2.say(); // child say demi 25
c2.sayFather(); // child sayFather demi 25
console.log(c2.colors); // ['red', 'blue']
console.log(c2.fatherColors); // ['green', 'yellow']
// 修改属性
c1.colors.push("black");
c1.fatherColors.push("black");
console.log(c1.colors); // ["red", "blue", "black"]
// 实例属性不共享 所以修改不会影响
console.log(c2.colors); // ["red", "blue"]
console.log(c1.fatherColors); //["green", "yellow", "black"]
// 原型链上引用数据类型的属性所有子类共享 容易造成修改混乱
console.log(c2.fatherColors); // ["green", "yellow", "black"]
// instanceof 属于子类实例
console.log("c1 instanceof Child:", c1 instanceof Child); // true
// 报错 不属于父类实例
// Uncaught TypeError: Right-hand side of 'instanceof' is not callable
// console.log(c1 instanceof Father);
构造函数继承
使用借用构造函数继承这种方式是通过在子类型的函数中调用父类型的构造函数来实现的。该继承方式有如下特点
- 构造函数继承解决了不能向父类型传递参数的缺点。
- 但是它存在的一个问题就是无法实现函数方法的复用,就是父类方法在每个实例里面都会存在,相较于原型继承浪费了存储空间。
- 并且父类型原型定义的方法子类型也没有办法访问到。
- 实例属于子类不属于父类。
// 父类
function Father(name, age) {
this.name = name;
this.age = age;
this.sayFather = function () {
console.log("child sayFather", this.name, this.age);
};
this.fatherColors = ["green", "yellow"];
}
const Hello = {
hello() {
console.log("hello", this.name, this.age);
},
helloArr: ["1", "2"],
};
// 父类的原型
Father.prototype = Hello;
//子类
function Child(name, age, sex) {
// 构造继承 可以传参 解决了不能向父类型传递参数的缺点
Father.call(this, name, age);
this.sex = sex;
this.colors = ["red", "blue"];
this.say = function () {
console.log("child say", this.name, this.age);
};
}
// c1
let c1 = new Child("randy", 24, "male");
console.log(c1);
c1.say(); // child say randy 24
// 能继承父类的方法
c1.sayFather(); // child sayFather randy 24
console.log(c1.colors); // ['red', 'blue']
// 能继承父类的属性
console.log(c1.fatherColors); // ['green', 'yellow']
// c2
let c2 = new Child("demi", 25, "female");
console.log(c2);
c2.say(); // child say demi 25
// 能继承父类的方法
c2.sayFather(); // child sayFather demi 25
console.log(c2.colors); // ['red', 'blue']
// 能继承父类的属性
console.log(c2.fatherColors); // ['green', 'yellow']
// 父类型原型定义的方法子类型也没有办法访问到
// c1.hello(); //报错 无法获取到父类原型上的方法
console.log(c1.helloArr); //undefined 无法获取到父类原型上的属性
// 修改属性 互不影响
c1.colors.push("black");
c1.fatherColors.push("black");
console.log(c1.colors); // ["red", "blue", "black"]
console.log(c2.colors); // ["red", "blue"]
console.log(c1.fatherColors); //["green", "yellow", "black"]
console.log(c2.fatherColors); // ["green", "yellow"]
// 修改方法 互不影响
c1.say = function () {
console.log("child update say", this.name, this.age);
};
c1.sayFather = function () {
console.log("child update sayFather", this.name, this.age);
};
c1.say(); //child update say randy 24
c2.say(); //child say demi 25
c1.sayFather(); // child update sayFather randy 24
c2.sayFather(); //child sayFather demi 25
// instanceof 实例不是父类的实例
console.log("c1 instanceof Child:", c1 instanceof Child); // true
console.log("c1 instanceof Father", c1 instanceof Father); // false
组合继承
组合继承是将原型链继承和构造函数继承组合起来使用的一种方式。通过借用构造函数的方式来实现实例属性的继承,通过将子类型的原型设置为父类的实例来实现原型属性的继承。该继承方式有如下特点
- 这种方式解决了上面的两种模式单独使用时的问题。能向父类型传递参数,能获取父类原型上的属性和方法。
- 由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数。
- 由于原型是父类实例,所以实例对象的原型中多了很多不必要的属性(实例中有父类的方法和属性,原型里面还有,都是重复的)。
- 实例对象既属于父类又属于子类。
// 父类
function Father(name, age) {
this.name = name;
this.age = age;
this.sayFather = function () {
console.log("child sayFather", this.name, this.age);
};
this.fatherColors = ["green", "yellow"];
}
// 原型对象
const Hello = {
hello() {
console.log("hello", this.name, this.age);
},
helloArr: ["1", "2"],
};
// 父类的原型
Father.prototype = Hello;
//子类
function Child(name, age) {
// 组合继承
Father.call(this, name, age);
this.colors = ["red", "blue"];
this.say = function () {
console.log("child say", this.name, this.age);
};
}
// 组合继承
Child.prototype = new Father();
Child.prototype.constructor = Child;
// c1
let c1 = new Child("randy", 24);
console.log(c1);
c1.say(); // child say randy 24
c1.sayFather(); // child sayFather randy 24
console.log(c1.colors); // ['red', 'blue']
console.log(c1.fatherColors); // ['green', 'yellow']
// c2
let c2 = new Child("demi", 25);
console.log(c2);
c2.say(); // child say demi 25
c2.sayFather(); // child sayFather demi 25
console.log(c2.colors); // ['red', 'blue']
console.log(c2.fatherColors); // ['green', 'yellow']
// 能获取父类原型方法
c1.hello(); // hello randy 24 能获取到父类原型上的方法
console.log(c1.helloArr); // ['1', '2'] 能获取到父类原型上的属性
// 修改原型上的引用数据类型 还是会改变所有实例
c1.helloArr.push("3");
console.log(c1.helloArr); // ["1", "2", "3"]
console.log(c2.helloArr); // ["1", "2", "3"]
// 修改实例属性 互不影响
c1.colors.push("black");
c1.fatherColors.push("black");
console.log(c1.colors); // ["red", "blue", "black"]
console.log(c2.colors); // ["red", "blue"]
console.log(c1.fatherColors); //["green", "yellow", "black"]
console.log(c2.fatherColors); // ["green", "yellow"]
// instanceof
console.log("c1 instanceof Child:", c1 instanceof Child); // true
console.log("c1 instanceof Father", c1 instanceof Father); // true
// 子类的原型会臃肿
// {name: undefined,age: undefined,sayFather: ƒ (),fatherColors: (2) ["green", "yellow"]}
console.log("子类的原型会臃肿 c1.__proto__", c1.__proto__);
寄生式继承
寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后创建一个新对象,该对象的原型是传入的对象。然后对该新对象进行扩展,最后返回这个新对象。这个扩展的过程就可以理解是一种继承。该继承方式有如下特点
- 这种继承的优点就是对一个简单对象实现继承。
- 没有办法实现函数的复用。
- 传入对象会被作为新对象的原型,会被所有的实例对象所共享,容易造成修改的混乱。
- 创建子类型的时候不能向超类型传递参数。
- 实例是父类的实例。
function CreateObj(obj) {
// 把传进来的对象作为新创建对象的原型
let newObj = Object.create(obj);
// 简单的一些扩展
newObj.say = function () {
console.log("say");
};
return newObj;
}
function Father(name, age) {
this.name = name;
this.age = age;
this.sayFather = function () {
console.log("child sayFather", this.name, this.age);
};
this.fatherColors = ["green", "yellow"];
}
let c1 = CreateObj(new Father("randy", 24));
console.log(c1);
c1.say(); // say
// 是父类的实例
console.log('c1 instanceof Father:', c1 instanceof Father); //true
寄生式组合继承
组合继承的缺点就是使用超类型的实例作为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用父类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。该继承方式是组合继承的升级版,有如下特点
- 原型在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。
- 创建子类型的时候能向超类型传递参数。
- 实例对象不再臃肿,原型只包含父类的原型。
- 实例对象既属于父类又属于子类。
function CreateObj(obj) {
// 把传进来的对象作为新创建对象的原型
let newObj = Object.create(obj);
return newObj;
}
// 父类
function Father(name, age) {
this.name = name;
this.age = age;
this.sayFather = function () {
console.log("child sayFather", this.name, this.age);
};
this.fatherColors = ["green", "yellow"];
}
// 原型对象
const Hello = {
hello() {
console.log("hello", this.name, this.age);
},
helloArr: ["1", "2"],
};
// 父类的原型
Father.prototype = Hello;
//子类
function Child(name, age) {
// 调用父类构造函数
Father.call(this, name, age);
this.colors = ["red", "blue"];
this.say = function () {
console.log("child say", this.name, this.age);
};
}
// 寄生式组合继承 使用父类的原型作为子类的原型
Child.prototype = CreateObj(Father.prototype);
Child.prototype.constructor = Child;
// c1
let c1 = new Child("randy", 24);
console.log(c1);
c1.say(); // child say randy 24
c1.sayFather(); // child sayFather randy 24
console.log(c1.colors); // ['red', 'blue']
console.log(c1.fatherColors); // ['green', 'yellow']
// c2
let c2 = new Child("demi", 25);
console.log(c2);
c2.say(); // child say demi 25
c2.sayFather(); // child sayFather demi 25
console.log(c2.colors); // ['red', 'blue']
console.log(c2.fatherColors); // ['green', 'yellow']
// 获取父类原型方法
c1.hello(); // hello randy 24 能获取到父类原型上的方法
console.log(c1.helloArr); // ['1', '2'] 能获取到父类原型上的属性
// 修改原型上的引用数据类型 还是会改变所有实例
c1.helloArr.push("3");
console.log(c1.helloArr); // ["1", "2", "3"]
console.log(c2.helloArr); // ["1", "2", "3"]
// 修改属性 互不影响
c1.colors.push("black");
c1.fatherColors.push("black");
console.log(c1.colors); // ["red", "blue", "black"]
console.log(c2.colors); // ["red", "blue"]
console.log(c1.fatherColors); //["green", "yellow", "black"]
console.log(c2.fatherColors); // ["green", "yellow"]
// instanceof 既是父类的实例又是子类的实例
console.log("c1 instanceof Child:", c1 instanceof Child); // true
console.log("c1 instanceof Child:", c1 instanceof Father); // true
// 子类的原型不会臃肿 只包含父类原型
// {hello: ƒ hello(),helloArr: (3) ['1', '2', '3']}
console.log("子类的原型不会臃肿 c1.__proto__", c1.__proto__);
class extends继承
除了使用ES5
的继承方式,我们还可以使用ES6
的class
来实现继承。
从上面class的介绍我们知道,只有方法才会被挂载到原型上,这是寄生式组合继承的升级版,除了有寄生式组合继承的优点外还解决了原型修改混乱的问题。这应该是最佳的继承方式了。
- 创建子类型的时候能向超类型传递参数。
- 实例既属于子类又属于父类。
- 实例对象不再臃肿,原型只包含父类的原型。
class Father {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 父类方法会被挂载到原型的原型上
sayFather() {
console.log("child sayFather", this.name, this.age);
}
}
// 子类继承
class Child extends Father {
_colors = ["blue", "red"];
constructor(name, age, sex) {
// 没参数
// super();
// 有参数
super(name, age);
this.sex = sex;
}
// 方法会挂载到原型上
say() {
console.log("child say", this.name, this.age, this.sex);
}
}
const c1 = new Child("randy", 24, "male");
console.log(c1);
console.log(c1._colors); // ['blue', 'red']
c1.say(); //child say randy 24 male
c1.sayFather(); // child sayFather randy 24
const c2 = new Child("demi", 25, "female");
console.log(c2);
console.log(c2._colors); // ['blue', 'red']
c2.say(); //child say demi 25 female
c2.sayFather(); // child sayFather demi 25
// 修改属性 互不影响
c1._colors.push("black");
console.log(c1._colors); // ["red", "blue", "black"]
console.log(c2._colors); // ["red", "blue"]
// instanceof 既是父类的实例又是子类的实例
console.log("c1 instanceof Child:", c1 instanceof Child); // true
console.log("c1 instanceof Child:", c1 instanceof Father); // true
console.log("子类的原型不会臃肿 c1.__proto__", c1.__proto__);
console.log(
"子类的原型不会臃肿 c1.__proto__.__proto__",
c1.__proto__.__proto__
);
看到这,JS原型、原型链和JS继承就讲完啦,感谢大家的耐心观看。小伙伴们是否弄明白了呢?
系列文章
后记
本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!