浅析JavaScript的继承和原型链

浅析JavaScript的继承和原型链

一、前言

JavaScript的继承和原型链是我在学习前端过程中遇到的较为少有的难以理解的部分,这里便将我所有知道和了解到的东西记录了下来,希望能够给还在这里面苦苦挣扎的兄弟萌一点点小的帮助,也欢迎各位大佬批评指正。

二、构造函数

2.1 构造函数的实例成员和静态成员

构造函数由实例成员和静态成员二者组成,其中实例成员是在函数内部通过this关键字添加的成员;只能通过实例化对象以后通过实例化对象进行访问;而静态成员是函数本身上添加的成员,只能通过构造函数来访问。

//创造一个构造函数
let Father = function(name,age){
    //实例成员
    this.name = name;
    this.age = age;
    this.method = "我是一个实例成员";
}
//静态成员
Father.like = "mother";
//检验实例对象是否能够被构造函数直接访问
console.log(Father.method);//undefined
console.log(Father.like);//mother
//实例化一个对象
let father = new Father("小王",27);
//检验静态对象是否能够被实例化对象访问
console.log(father.name);//小王
console.log(father.age);//27
console.log(father.like);//undefined
2.2 实例化对象的过程

通过new关键字可以通过构造函数实现一个实例化对象,那么在具体实例化的过程中发生了什么呢?大致可以划分为以下几个步骤:

(1) 创建一个空对象 son {}

(2) 为 son 准备原型链连接 son.__proto__ = Father.prototype

(3) 重新绑定this,使构造函数的this指向新对象 Father.call(this)

(4) 为新对象属性赋值 son.name

(5) 返回this return this,此时的新对象就拥有了构造函数的方法和属性了

一个小问题:所有实例化对象的方法都是共享的吗?

构造函数的方法分为两种,第一种为在函数内部直接定义的方法,第二种为通过原型添加的方法;

//函数内部直接定义的方法
let Father = function(){
    this.read = function(){
        console.log("我是内部定义的read方法!");
    }
}
//通过原型链添加的方法
Father.prototype.look = function(){
    console.log("我是通过原型链定义的look方法!");
}

//实例化对象进行检验
let father1 = new Father();
let father2 = new Father();
father1.read();//我是内部定义的read方法!
father2.read();//我是内部定义的read方法!
console.log(father1.read === father2.read);//false
father1.look();//我是通过原型链定义的look方法!
father2.look();//我是通过原型链定义的look方法!
console.log(father1.look === father2.look);//true

可以发现,函数内部直接定义的方法在每实例化一个新的对象以后,都会给这个方法分配一个新的内存空间,而通过原型添加的方法便会共享一个空间。

一个小问题:所有实例化对象的属性都是共享的吗?

不存在内存空间的问题,判断时看其值是否相同;

let Father = function(name){
    this.name = name;
}
let father1 = new Father("小王");
let father2 = new Father("小红");
console.log(father1.name === father2.name);//false
let father1 = new Father("小王");
let father2 = new Father("小王");
console.log(father1.name === father2.name);//true

因此我们可以总结一下定义构造函数的基本规则,即公共属性定义到构造函数里面,公共方法我们放到原型对象身上。

三、原型

3.1 什么是原型

Father.prototype 就是原型,它是一个对象,也可以称为原型对象。

3.2 原型的作用是什么

原型的作用,就是共享方法。

我们通过 Father.prototype.method 可以共享方法,不会反应开辟空间存储方法。

3.3 原型中的this指向哪儿

原型中this的指向是实例。

四、原型链

原型链本人感觉是一个对于初学者或者说是部分前端菜鸡(例如本人)来说特别难以理解的东西,为了让下面的部分更容易理解,这里强行先记住以下几点:

  1. __proto__是每个对象都有的属性,prototype是每个函数特有的方法;
  2. 每个对象的__proto__属性都会指向自身构造函数的prototype;
  3. constructor属性始终指向创建当前对象的构造函数;
  4. Function.__proto__ === Function.prototype;
  5. Object.prototype.__proto__ === null 也就是原型链的终点;
4.1 什么是原型链

原型与原型层层相链接的过程即为原型链。

4.2 原型链的应用

对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在每个对象都有__proto__原型的存在

let Father = function(name){
    this.name = name;
}
let father = new Father("老王");
console.log(father.__proto__ === Father.prototype);//true
//验证上述说法中的第二条
4.3 原型链图

原型链

结合写在最前面的几点,理解上图应该问题不大了,图中圈起来的部分就是骇人听闻的原型链。

4.4 原型链的查找方式
function Star(name) {
	this.name = name;
	//(1)首先看obj对象身上是否有dance方法,如果有,则执行对象身上的方法
	this.dance = function () {
		console.log(this.name + '1');
	}
}
//(2)如果没有dance方法,就去构造函数原型对象prototype身上去查找dance这个方法。
Star.prototype.dance = function () {
	console.log(this.name + '2');
};
//(3)如果再没有dance方法,就去Object原型对象prototype身上去查找dance这个方法。
Object.prototype.dance = function () {
	console.log(this.name + '3');
};
//(4)如果再没有,则会报错。
let obj = new Star('小红');
obj.dance();

(1)首先看obj对象身上是否有dance方法,如果有,则执行对象身上的方法。

(2)如果没有dance方法,就去构造函数原型对象prototype身上去查找dance这个方法。

(3)如果再没有dance方法,就去Object原型对象prototype身上去查找dance这个方法。

(4)如果再没有,则会报错。

一个小问题:在原型上添加方法需要注意的地方

有两种添加方法,第一种为上面的写法,直接通过 构造函数.prototype.方法名 进行添加;第二种为重定义构造函数的prototype,但是此种情况会丢失掉原有的constructor构造器,所以一定要再连接回去,例子如下:

function Star(name) {
	this.name = name;
}
Star.prototype = {
    dance:function(){
    	console.log("重定义prototype");
	}
}
Star.prototype.constructor = Star;

另外,类似于Array、String这些内置的类是不能这么处理的。

五、继承

这里就长话短说,首先我们要明确继承需要继承哪些东西,在前文中我们提到了定义构造函数的基本规则,即**公共属性定义到构造函数里面,公共方法我们放到原型对象身上。**我们所需要继承的东西也不外乎就这二者,公共属性的继承可以通过call()或者apply()进行this的指向定义,而公共方法可以通过原型对象的赋值进行处理,因此我们很容易想到如下的方法:

//定义一个父类
function Father(name) {
	this.name = name;
}
Father.prototype.dance = function () {
	console.log('I am dancing');
};
//定义一个子类
function Son(name, age) {
	Father.call(this, name);
	this.age = age;
}
//通过赋值的方法连接
Son.prototype = Father.prototype;
//为子类添加方法
Son.prototype.sing = function () {
	console.log('I am singing');
};
let son = new Son('小红', 100);
//此时父类也被影响了
console.log(Father.prototype) //{dance: ƒ, sing: ƒ, constructor: ƒ}

很显然,当我们只想修改子类里面的方法时,显然上述方法不太合适;因此 我们可以尝试new一个新的父类出来,代码如下:

function Father(name) {
	this.name = name;
}
Father.prototype.dance = function () {
	console.log('I am dancing');
};
function Son(name, age) {
	Father.call(this, name);
	this.age = age;
}
Son.prototype = new Father();
Son.prototype.sing = function () {
	console.log('I am singing');
};
let son = new Son('小红', 100);
console.log(Father.prototype) //{dance: ƒ, constructor: ƒ}

六、class语法糖

对于以前了解过面向对象编程的程序员来讲,上述关于继承的写法属实让人有些难以接受,因此在es6里面新增了一个语法糖来更方便更便捷地书写继承,这里就直接上代码了;

class Father {
	constructor(name) {
		this.name = name;
	}
	dance() {
		console.log("我是" + this.name + ",我今年" + this.age + "岁," + "我在跳舞");
	}
}
class Son extends Father {
	constructor(name, age) {
		super(name);
		this.age = age;
	}
	sing() {
		console.log("我是" + this.name + ",我今年" + this.age + "岁," + "我在唱歌");
	}
}
let obj = new Son('小红', 19);
obj.sing();
obj.dance();

分析一下上面代码,首先一个类(构造函数)里面依旧为两部分,即公共属性和公共方法,constructor() 里面存放了该构造函数的公共属性,后面接着的便是公共方法,extends 关键字表示继承的是哪个类,super() 便是将里面父类里面相应的公共属性拿出来,这样看下来便可以将代码规整许多。

以上便是本人的一些拙见,欢迎各位大佬批评指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值