JavaScript之class详解

浅谈面向对象

为什么说是浅谈呢,一是我本身对于这个面向对象的理解就不太深刻,二是关于本文的主题 class 并不需要很深入讲解,只能将自己的理解的一部分表达出来,那为什么要说呢?因为需要聊一下这个才能开展后续的讲解

如有错误,还请指正

面向对象简介

  1. 面向对象(Object-Oriented)是一种编程范式
  2. 在面向对象中,一个事物可以有那些状态,称之为属性,而有那些行为,称之为方法,比如:一个人具备的属性就是他的名字、年龄、身高、体重这都可以看成是人的属性,而这个人具备吃饭、运动、学习等等这样的行为,就可以看成是这个人的方法

类和对象之间的关系

  1. 关于这个,我们可以举一个例子来看一下,比如说车,但是小汽车、大卡车、越野车都是车啊,但是这些小汽车、大卡车、越野车都是不一样的名称为什么可以都叫车呢
  2. 我们细想一下就可以发现,车只是一个概念,是这一类事物的统称或者说类,并不指代单一的某个具体的事物
  3. 这也是面向对象的基础观念,在面向对象的概念中,认为世界是由很多具体的事物组成的,而每一个具体的事物它会属于一个分类,这些每一个具体的事物就称之为对象,比如小汽车、大卡车,而车这个统称就被称之为,每一个类都可以有无数个对象,但是作为类只能有一个
  4. 那为什么需要类呢,直接描述一个具体的事物行不行呢,如果没有类,以后你要描述出一个小汽车,就要描述一大段话,小汽车可以跑,可以载一些人,可以让我们日行千里,它还有轮子等等,你在描述大客车时又要在描述一次,而这些描述具备很多重复的东西,比如载人,比如跑的快,比如有轮子,但是如果我们有类,我们就不需要去重复描述这么多次,小汽车,大客车,我们需要的描述的就只是一些不同点,比如载人多少的数量的区别而已,类就是用来描述具有相同特征的对象
  5. 一个语言想要使用面向对象的开发模式,最基本的就是需要能够表达出类和对象

面向对象的主要特点

  1. 封装:将数据和操作数据的方法封装在一个对象中,隐藏对象的属性和实现细节,开放一些对外的公共访问方式,让外界可以和内部产生一些联系,简单说一点就是,你想看到的都是我想让你看到的,举个例子就是你正在使用的手机,你只需要知道你解锁就可以打开屏幕,点击相机就可以拍照,但是内部怎么实现不需要我们关心,使得对象具备独立性复用性
  2. 继承:通过继承的这个机制,可以在已有的类的基础上派生出一个新类,还是我们之前的例子,一个车类可以衍生一个小汽车类和大卡车类等等,而通过这种继承关系的,一般也会称之为子类与父类,子类会继承父类的属性和方法,使子类可以使用父类的相同的行为
  3. 多态:同一个方法可以被不同的对象调用,从而实现代码的灵活性和可扩展性,比如说铅笔和钢笔,都可以进行书写

面向对象 vs 面向过程

  1. 那面向对象对比传统的面向过程到底具备什么优势呢,可以让面向对象称为主流的设计方案
  2. 我们用一个贪吃蛇的游戏来简单说明一下,如果是面向过程我们考虑的是,(1)创建地图并添加积分面本和游戏开始和结束,(2)地图生成一条蛇,(3)创建一个在地图上随机生成的食物,(4)让蛇进行移动,(5)判断这个蛇有没有吃到食物,(6)吃到食物则积分+1,(7)出现下一个食物
  3. 而如果换成一个面向对象的方式,我们就先会设计一个地图类,包含积分面板、开始、结束;然后设计蛇类,包括蛇的初始位置,移动方法、是否吃到食物方法;在设计食物类游戏规则类
  4. 面向过程是步骤化,而面向对象是模块化
  5. 面向过程的优点很明显,性能比较好一些,不需要进行类的实例化,但是缺点也是显而易见的,不利于维护、扩展、复用
  6. 面向对象易维护、易复用、易扩展,也因为封装、继承、多态的特性,可以让系统直间的耦合性降低
  7. 因此在搭建一定规模项目的时候,这时候使用面向对象是优于面向过程的

在 JavaScript 中表达类和对象

  1. 在 JavaScript 中的 ES5 中可以使用构造函数表达,但是显然这个语义化不是很好,因此在 ES6 中为我们提供了一个语法 class,如下:

    // 表达类
    class Car{}
    
  2. 那表达一个一个对象呢?可以通过实例化这个类表达,如下:

    class Car {}
    
    const c1 = new Car()
    const c2 = new Car()
    const c3 = new Car()
    
  3. 在面向对象中,我们是可以得知某一个对象是属于那一个类的,我们在 JavaScript 中可以通过原型来得知这个问题,这也是在 JavaScript 中会出现原型非常重要的因素,具体为什么原型可以得知这里就不赘述了,不理解的可以翻看我之前的文档JavaScript原型链详解,测试如图:
    在这里插入图片描述

  4. 通过上图我们可以知道 c1 属于 Car 这个类,也可以通过判断等于 Car 的原型得知属于这个类

属性和方法

在前面我们提到过,类是用来描述对象的相同特性,那么车一般具有什么特征,比如颜色、重量、大小、可以跑、可以载人或者载货,这些共同的特征就会在类中描述而非对象,且面向对象中认为某一个对象的特征分为两部分,属性和方法,属性为名词,如:颜色、重量、大小,而方法为动词,如:可以跑、可以载人或者载货

  1. 了解了属性和方法,我们就可以来设计一个类,如下:

    class Car {
    	// 颜色
    	color
    	// 大小
    	size
    	// 重量
    	weight
    	// 跑
    	run() {
    		console.log('跑')
    	}
    	// 载人
    	manned() {
    		console.log('可载人')
    	}
    }
    
    const c = new Car()
    
  2. 通过这个类创建的对象就会自动有用这个类的所有属性和方法,结果如图:
    在这里插入图片描述

  3. 此时这个对象也可以添加自己的属性和方法,比如加一个别名属性,代码如下:

    const c = new Car()
    c.alias = '小汽车'
    
  4. 看一下是否具备我们添加的属性,结果如图:
    在这里插入图片描述

  5. 这时候我们又面临一个解决的问题,虽然定义了这些特征车有颜色,但是没有详细的描述这些特征比如颜色、大小、重量,那颜色是什么、大小和重量是多少,而且不同的车具备的颜色这些又可能不一样,在上图的测试中都是 undefined,而这些行为,只是车可能有这个行为的能力,但是有不代表会一定就使用,需要具体的车去使用这些方法,在面向对象中处理这个问题在类中有两个时间点,一个是创建对象时,一个是动作产生时,因此我们可以在对象产生时我们如何描述它的特征,还可以在一个某个动作发生的过程中来描述它的特征

  6. 而在对象创建时它的描述行为称之为构造函数,表示某一个对象产生了,可以把特征描述在构造函数里面,当一个对象开始跑起来做出这种具体的行为的时候,就可以把特征描述在方法里面,如下:

    class Car {
    	color
    	size
    	weight
    	constructor() {
    		// 对象创建时描述特征
    	}
    	run() {
    		// 对象执行某一个行为时描述特征
    	}
    	manned() {}
    }
    
  7. 因此我们描述的并不是某一个类,而是描述在未来某一辆车制造完成时,某一辆车执行行为时,也因此我们需要在类中知道是某一辆车制造出来了,此时这个车就会具备颜色、大小等等这些特征,而这个某一辆在代码中的表现就是 this,而当某一辆车制造出来时或者执行某些行为时所产生的未知信息就是参数,因此我们可以对类进行一些修改,如下:

    class Car {
    	color
    	size
    	weight
    	// 通过参数确定创建时车的特征信息
    	constructor(color, size, weight) {
    		this.color = color
    		this.size = size
    		this.weight = weight
    	}
    	run() {}
    	manned() {}
    }
    
    const c1 = new Car('黑色', 'big', 50)
    const c2 = new Car('白色', 'min', 70)
    console.log('c1', c1)
    console.log('c2', c2)
    
  8. 我们可以看一下结果,如图:
    在这里插入图片描述

  9. 但是也不是所有的都需要参数来传递,比如有些已知的信息是不需要传递的,只有未知的信息才需要通过参数传递,比如这两车已经被制造出来多少年了,这个创建出来时应该都是不足一年的,可以为 0,如下:

    class Car {
    	color
    	size
    	weight
    	age
    	constructor(color, size, weight) {
    		this.color = color
    		this.size = size
    		this.weight = weight
            // 已知的信息可以不需要传递
    		this.age = 0
    	}
    	run() {}
    	manned() {}
    }
    
  10. 而这种在类中描述某一个对象的属性和方法可以称之为对象属性或者对象方法,也可以称之为实例属性实例方法,某个类的对象也称之为某个类的实例,实例指的就是某一个概念下的一个具体的例子,类就是概念,对象就是例子

面向对象开发实例

  1. 现在我们存在一个需求,我们需要做一个智能灯的控制功能,
  2. 这个灯我们可以定义三个属性颜色、功率大小,开关的定时时间段,有六个方法,增加定时方法、删除定时方法、修改定时方法、获取定时方法、执行定时方法、对定时执行排序方法
  3. 在我们定义的需求里面定时排序方法执行定时排序方法定时属性我们并不需要暴露给外面,因此应该为私有属性和方法,在JavaScript中新出的 # 可以实现这一点,不过我们在这里使用 _开头表示是私有方法和属性
  4. 如果创建时不传入定时在0点是打开
  1. 现在我们需求明确之后,可以定义一个灯的类,如下:

    class lamp {
    	constructor(color, power, tim = [{ time: 0, switch: true }]) {
    		this.color = color
    		this.power = power
    		this._tim = tim
    		this._timer = null
    		this._timedSort()
    		this._executeTim()
    	}
    
    	decrease(index) {
    		this._tim.splice(index, 1)
    		this._timedSort()
    	}
    
    	increase(tim) {
    		this._tim.push(tim)
    		this._timedSort()
    	}
    
    	patchTim(index, time) {
    		this._tim[index] = time
    		this._timedSort()
    	}
    
    	getTim() {
    		return this._tim
    	}
    
    	_executeTim() {
    		// 由于是模拟测试所以就按照每一秒执行一个定时,不在进行检测是否时间到达执行
    		let i = 0
    		this._timer = setInterval(() => {
    			this._tim[i].switch ? console.log(`${this._tim[i].time}的时候-灯开了~芜湖!`) : console.log(`${this._tim[i].time}的时候-灯关了~啊哦!`)
                
    			i++
    			if (i === this._tim.length) {
    				i = 0
    				clearInterval(this._timer)
    				this.timer = null
    			}
    		}, 1000)
    	}
    
    	_timedSort() {
    		this._tim.sort((a, b) => {
    			return a.time - b.time
    		})
    	}
    }
    
  2. 当然,这只是为了演示,所以很多逻辑没有完善,现在我们创建一个灯,如下:

    // 创建灯
    const l = new lamp('蓝色', '40w', [
    	{ time: 4, switch: false },
    	{ time: 3, switch: true }
    ])
    
  3. 输出效果如图:
    在这里插入图片描述

  4. 现在来增加两个定时看看输出效果,代码如下:

    const l = new lamp('蓝色', '40w', [
    	{ time: 4, switch: false },
    	{ time: 3, switch: true }
    ])
    l.increase({ time: 1, switch: true })
    l.increase({ time: 6, switch: false })
    
  5. 输入效果如图:
    在这里插入图片描述

  6. 后面的删除修改就不在演示了,调用方法传入参数即可

实例成员和静态成员

  1. 什么是实例成员呢,我们可以先思考一个问题,定义在类中的属性和方法是谁在使用,是我们实例化之后得到的对象在使用,也因此通过这个对象调用的属性和方法就是实例成员

  2. 为什么会这么定义呢?就像在我们的生活中,你无法直接说车是什么颜色,让车跑起来,你只能说这个车是黑色,这个车跑起来了,只能是一个具体的事物,而非是一个类,所以通过实例化之后的对象调用的属性被称之为实例成员

  3. 什么是静态成员呢?比如车的总数,我们可以说车的总数是多少,而这个总数在有一个具体的事物去描述就不合适了,因此是属于这个类的属性,直接通过类可以访问的属性和方法就是静态成员

  4. 那静态成员怎么实现呢,有一个关键字 static,通过这个关键词修饰的属性和方法就是静态成员,如下:

    class Car {
    	color
    	size
    	weight
    	age
    	static total = 0
    	constructor(color, size, weight) {
    		this.color = color
    		this.size = size
    		this.weight = weight
    		this.age = 0
    		Car.total++
    	}
    	run() {}
    	manned() {}
    }
    
    const car1 = new Car('red', 'big', 100)
    const car2 = new Car('blue', 'big', 200)
    
    console.log(Car.total) // 2
    

继承-父类与子类

  1. 在 class 中实现继承非常简单使用关键字 extends 即可,如果需要父类传递参数,使用 super 方法即可

  2. 代码如下:

    class Car {
    	static total = 0
    	constructor(color, size, weight) {
    		this.color = color
    		this.size = size
    		this.weight = weight
    		this.age = 0
    		Car.total++
    	}
    
    	get upkeepCost() {
    		return this.age * 100
    	}
    
    	run() {
    		console.log('running')
    	}
    	manned() {}
    }
    
    class Bicycle extends Car {
    	constructor(name, age, sno) {
    		super(name, age) // 调用父类构造函数
    		this.sno = sno
    	}
    }
    
    const b = new Bicycle('#fff', 20, 1)
    b.run() // running
    

面试题-class降级处理

将一个 ES5 的类转为 ES5 的写法

基础降级

  1. 我们先来看一下降级后的 ES5 代码,如下:

    // 等价于 class 类的 constructor 函数
    function Person(name, age) {
    	// 实例成员
    	this.name = name
    	this.age = age
    	// 静态成员
    	Person.count++
    }
    
    Person.count = 0
    
    Person.prototype.runing = function () {
    	console.log(`${this.name} 在跑步~`)
    }
    
  2. 这一段代码应该不难理解,首先我们通过 new Person() 创建实例对象的时候,就会进行参数赋值,实现 class 的 constructor 的初始化,在原型上挂在的方法,实例化之后的对象也可以使用,我们可以使用代码测试一下,如下:

    const p1 = new Person('张三', 18)
    const p2 = new Person('李四', 20)
    console.log(p1.name) // 张三
    console.log(p2.name) // 李四
    p1.runing() // 张三 在跑步~
    p2.runing() // 李四 在跑步~
    console.log(Person.count) // 2
    

暂时性死区

我们可以先简单的说一下上面是暂时性死区:

​ JavaScript中的暂时性死区(Temporal Dead Zone,简称TDZ)指的是在一个代码块(例如函数、if语句块、for循环等)中,使用let或const声明的变量在声明前无法被访问和使用,即使在代码块中出现在声明语句之前的位置也是如此

使用var声明的变量没有暂时性死区的概念,因为var声明的变量会被默认提升到函数或全局作用域的顶部

  1. 先看一下如果提前调用 class 的话会有怎么样的错误,如图:
    在这里插入图片描述

  2. 通过这个报错可以很明显的看到, Car 是不允许被在初始化之前被访问的,那现在我们在看一下我们现在实现的基础降级能否提前访问,如图:
    在这里插入图片描述

  3. 可以看到是依然可以正常访问的,这是因为函数存在提升,我们不可以在 ES5 的环境下完全模拟 ES6 的暂时性死区,不过可以通过将函数这块使用自执行函数赋值一个变量,让他的函数提升消失,代码如下:

    var person = (function () {
    	function Person(name, age) {
    		console.log('被访问了~')
    		// 实例成员
    		this.name = name
    		this.age = age
    		// 静态成员
    		Person.count++
    	}
    
    	Person.count = 0
    
    	Person.prototype.runing = function () {
    		console.log(`${this.name} 在跑步~`)
    	}
    	return Person
    })()
    
  4. 测试结果如图:
    在这里插入图片描述

  5. 这样在初始化之前访问的就是一个 undefined,访问 undefined 抛出一个非构造函数错误,如果使用 let 和 const 就会简单一些,但是 let 和 const 也是 ES6 的语法

实现构造函数必须使用 new 调用

  1. 我们虽然写的是一个构造函数,但是这个函数依然是可以被当成普通函数调用的,因此我们需要在内部增加一个判断,判断是否通过 new 调用的本构造函数,如果没有通过 new 调用,则需要抛出一个错误,ES6 提供了一个语法 new.target,这个语法可以很快的判断是否 new 调用,不过降级,就不能使用任何的 ES6 语法

  2. 因此我们需要先看一下通过 new 调用和 普通调用时这个 this 指向的是谁,先看一下直接调用的 this 指向输出,如图:
    在这里插入图片描述

  3. 函数独立调用指向 window 这个应该都知道,我们在看一下 new 调用时的 this 指向,如图:
    在这里插入图片描述

  4. new 调用指向的就是 Person 构造函数,因为实例化时,就会将构造函数本身的原型赋值给实例化对象的隐式原型对象,这是关于原型的知识这里就不在赘述了,因此我们只需要判断构造函数调用时当前的 this 指向是否等于当前构造函数的原型即可,等于就是 new 调用,否则我们应该抛出一个错误 Cannot access 'Person' before initialization,我们知道实例化对象具备隐式原型即__proto__,也因此可以通过 this.__proto__ 是否等于 Person.prototype 来判断,不过__proto__并非 ESMA 的标准属性,是浏览器添加的,所以我们应该使用 Object*.*getPrototypeOf() 方法代替,这个方法的具体讲解我们不在叙述了,可以自行查看 MDN 文档Object*.*getPrototypeOf(),代码如下:

    var Person = (function () {
    	function Person(name, age) {
    		if (Object.getPrototypeOf(this) !== Person.prototype) {
    			throw new Error('Cannot access "Person" before initialization')
    		}
    		this.name = name
    		this.age = age
    		Person.count++
    	}
    
    	Person.count = 0
    
    	Person.prototype.runing = function () {
    		console.log(`${this.name} 在跑步~`)
    	}
    	return Person
    })()
    
  5. 我们在看一下调用的结果,如图:
    在这里插入图片描述

  6. 虽然没有传递参数,但是达到了我们需要的效果

实现访问器

  1. 什么是访问器呢,即一个 get 方法,其实也比较简单,当你有一个是需要依赖某些属性计算才能得出的时候,使用这个访问器就会方便很多,比如我们的车每年的保养费用都会随着出厂年限增加,我们可以在代码中具体测试一下,如下:

    class Car {
    	static total = 0
    	constructor(color, size, weight) {
    		this.color = color
    		this.size = size
    		this.weight = weight
    		this.age = 0
    		Car.total++
    	}
    
    	get upkeepCost() {
    		return this.age * 100
    	}
    
    	run() {}
    	manned() {}
    }
    const c = new Car('red', 'big', 1000)
    // 将年限改为 3 年
    c.age = 3
    // 获取保养费用
    console.log(c.upkeepCost) // 300
    
  2. 通过 get 定义的函数是可以当做属性调用的,而这种名词的特征也理应是一个属性,有点类似 Vue 的 computed,如果我们需要实现这个效果,需要用到一个知识,属性描述符,如果对这个不了解的可以翻看我的另外一篇文档,里面包含了属性描述符的介绍,这也是 Vue2 实现数据响应式的关键,文档地址:Vue2响应式实现原理和解析,我们现在需要做的就是在构造函数的原型上定义一个属性,这个属性通过属性描述拦截并计算,比如年龄越大在一段时间内身高会越高,代码如下:

    var Person = (function () {
    	function Person(name, age) {
    		if (Object.getPrototypeOf(this) !== Person.prototype) {
    			throw new Error('Cannot access "Person" before initialization')
    		}
    		this.name = name
    		this.age = age
    		Person.count++
            Object.defineProperty(this, 'height', {
    			enumerable: false, // 不可枚举 | 不写默认也是 false 
    			get: function () {
    				return this.age * 10 + 'cm'
    			}
    		})
    	}
        
    	// 在原型上添加一个身高属性
    	Object.defineProperty(Person.prototype, 'height', {
    		get: function () {
    			return this.age * 10 + 'cm'
    		}
    	})
    
    	Person.count = 0
    
    	Person.prototype.runing = function () {
    		console.log(`${this.name} 在跑步~`)
    	}
    	return Person
    })()
    
  3. 输出如图:
    在这里插入图片描述

方法不可以枚举且不能使用 new 调用

  1. 先看一下当前的方法,如图:
    在这里插入图片描述

  2. 上图的结果我们可以看到是可以枚举的,在类中定义的实例方法是不可枚举的,因此我们还需要属性描述符再次定义一下这些方法,如下:

    var Person = (function () {
    	function Person(name, age) {
    		if (Object.getPrototypeOf(this) !== Person.prototype) {
    			throw new Error('Cannot access "Person" before initialization')
    		}
    		this.name = name
    		this.age = age
    		Person.count++
    		Object.defineProperty(this, 'height', {
    			enumerable: false, // 不可枚举
    			get: function () {
    				return this.age * 10 + 'cm'
    			}
    		})
    	}
    
    	Object.defineProperty(Person.prototype, 'height', {
    		get: function () {
    			return this.age * 10 + 'cm'
    		}
    	})
    
    	Person.count = 0
    
    	// 使用属性描述符重新定义方法
    	Object.defineProperty(Person.prototype, 'runing', {
    		enumerable: false,
    		value: function () {
    			console.log(`${this.name} 在跑步~`)
    		}
    	})
    
    	return Person
    })()
    
  3. 看一下结果,如图:
    在这里插入图片描述

  4. 现在我们再来 ES6 中看一下 new 调用是什么效果,如图:
    在这里插入图片描述

  5. 在 ES5 中同样调用看一下结果,如图:
    在这里插入图片描述

  6. 可以看到执行了函数,那么这种情况如何处理呢,还是一样看一下 p1.runing() 与 new p1.runing() 的 this 指向区别,如图:
    在这里插入图片描述

  7. 可以看到通过 p1.runing() 方式调用 this 指向的依然是 Person 构造函数,但是通过 new 调用指向的就是 runing 这个函数本深的原型了,因此只要判断出当前调用时它的指向指向当前 runing 函数本身就是 new 调用同时抛出错误,代码如下:

    // 使用属性描述符重新定义方法
    Object.defineProperty(Person.prototype, 'runing', {
    	enumerable: false,
    	value: function () {
    		if (Object.getPrototypeOf(this) === Person.prototype.runing.prototype) {
    			throw new Error('runing is not a constructor')
    		}
    		console.log(`${this.name} 在跑步~`)
    	}
    })
    
  8. 我们测试一下结果,如图:
    在这里插入图片描述

继承

好了,到了降级的关键了,前面的降级处理可能都是比较容易理解的,那么这个继承就需要对原型链有着更加深刻的认识,那为什么需要有继承呢?这点最初的面向对象说继承那块我觉得已经很好的解释了

  1. 要实现继承我们肯定就需要有着一个父类和子类,在下面的例子中,我们除了有 Person 这个父类还应该有一个 Student 子类,按照继承的构想我们的 name 和 age属性应该都源自于父类,所以学生类我们目前定义一个学号属性和一个学习方法,子类构建如下:

    function Student(name, age, sno) {
    	this.sno = sno
    }
    
    Student.prototype.studying = function () {
    	console.log(`${this.name} 正在学习`)
    }
    
  2. 那么如何实现这父类与子类的的继承关系呢?我们可以先看一下两者的关系图,如图:
    在这里插入图片描述

  3. 从上图我们可以看出的基本信息就是现在父类和子类都具备自己的原型对象,并且没有任何的联系,我们先来实现第一步,让使用 new Student(‘张三’, 18, 1111) 创建学生信息的时候,name 属性和 age 属性的赋值交给父类 Person 去处理,我们就可以在此处借用 Person 函数内部的代码逻辑,通过改变 this 指向让它帮助我们进行 name 和 age 属性的赋值。代码如下:

    var Student = (function () {
    	function Student(name, age, sno) {
    		// 调用 Person,并使用 call 改变 this 指向
    		Person.call(this, name, age)
    		this.sno = sno
    	}
    
    	Student.prototype.studying = function () {
    		console.log(`${this.name} 正在学习`)
    	}
    	return Student
    })()
    
  4. 需要注意的是这里使用 Person.call 就不能使用之前的判断来判断是否是 new 调用了,应该改为用原型链查找的方法,我们应该通过判断当前调用时的对象是否存在 prototype 这个原型对象,如下:

    // 实现一个 instanceOf 方法
    //  - obj 检测的对象
    //  - constructor 检测的构造函数
    function myInstanceOf(obj, constructorFunc) {
       // 检查 obj 是否为 null 或 undefined
       if (obj === null || obj === undefined) {
       	return false
       }
       // 获取 obj 的原型对象
       let proto = Object.getPrototypeOf(obj)
       // 如果 obj 的原型对象与构造函数的原型对象相同,则返回 true
       if (proto === constructorFunc.prototype) {
       	return true
       }
       // 递归查找 obj 的原型链上的所有原型对象
       return myInstanceOf(proto, constructorFunc)
    }
    
    var Person = (function () {
       function Person(name, age) {
       	if (!myInstanceOf(this, Person)) {
       		throw new Error('Constructor must be called with new keyword')
       	}
       	this.name = name
       	this.age = age
       	Person.count++
       }
    })()
    
  5. 为什么说效果一样,只是从借用这个赋值过程变成了手动赋值,通俗一点就是,格调降低了,我们现在在关心一下下一步,现在属性赋值没有什么问题了,那 runing 这个方法,只有父类才有,子类没有,怎么实现呢,我们首先的思路就是创建一个对象,让这个对象的隐式原型指向父类的原型,并且把这个对象挂载到我们子类的原型上,这样就可以让子类使用父类的方法,于是我们可以得出一个关系图,如下:
    在这里插入图片描述

  6. 从上图我们可以看舍去了 Student 的原型对象,让 Temp 构造函数的原型对象指向了 Person 的原型对象,这样 Student 的原型对象指向 Temp 的原型对象,就行了一个链条的关系,为什么非要通过一个中间的构造的函数呢,如果没有这个中间的构造函数,直接使用 new Person() 得到一个实例对象并赋值给 Student 的原型,会有什么影响呢,会有一个 age 和 name 属性值为 undefined 的影响,因为 new Person() 没有传递参数导致是一个 undefined,如图:
    在这里插入图片描述

  7. 所以我们需使用一个中间对象来处理,也因此我们需要编写一个函数,传入一个父类的原型对象,返回一个对象,这个对象的原型指向到这个父类原型,因此我们可写出如下函数:

    function createObject(o) {
    	// 也可以使用 Object.create() 和  Object.setPrototypeOf() 方法
    	function Temp() {}
    	Temp.prototype = o
    	return new Temp()
    }
    
  8. 使用这个函数,代码如下:

    var Student = (function () {
    	function Student(name, age, sno) {
    		this.name = name
    		this.age = age
    		this.sno = sno
    	}
    
    	Student.prototype = createObject(Person.prototype)
    
    	Student.prototype.studying = function () {
    		console.log(`${this.name} 正在学习`)
    	}
    	return Student
    })()
    
  9. 效果如图:
    在这里插入图片描述

  10. 到现在好像基本没有问题了,但是如果我们换一个环境执行呢,在 node 中执行,如图:
    在这里插入图片描述

  11. 在 node 中输出的就是 Person 类,至于为什么会有这个差别呢,我个人的猜测就是因为在浏览器中,console.log()方法通常会显示对象的构造函数名称,而在Node.js中,console.log()方法通常会显示对象的属性和方法,并将它们作为Person类的属性进行显示,不过如果要解决这个问题也很简单,在设置一下 prototype 中的 constructor 即可,如下:

    var Student = (function () {
    	function Student(name, age, sno) {
    		this.name = name
    		this.age = age
    		this.sno = sno
    	}
    
    	Student.prototype = createObject(Person.prototype)
    	Object.defineProperty(Student.prototype, 'constructor', {
    		enumerable: false, // 不可枚举
    		writable: false, // 不可重写
    		value: Student
    	})
    
    	Student.prototype.studying = function () {
    		console.log(`${this.name} 正在学习`)
    	}
    	return Student
    })()
    
  12. 在看一下输出的结果,如图:
    在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值