前言
JavaScript在解决复用性方面做过很多尝试,最终确定了利用原型和原型链来解决。
而在ES6之前,JavaScript 中除了基础类型外的数据类型都是对象(引用类型),没有类(class),为了实现类似继承以便复用代码的能力,JavaScript选择了原型和原型链。
甚至在ES6之后,JavaScript也没有真正的类(class)。ES6虽然提供了class关键字让我么可以伪造一个“类”,但其实只是语法糖而已,本质上仍然是一个对象。
ES6实现的继承,本质仍是基于原型和原型链。
构造函数
❝使用new运算符的函数对象
❞
function Star(name,age) {
//实例成员
this.name = name;
this.age = age;
}
//静态成员
Star.sex = '女';
let stars = new Star('小红',18);
console.log(stars); // Star {name: "小红", age: 18}
console.log(stars.name); //小红 通过构构造对象访问实例成员
console.log(Star.sex); //女 通过构造函数可直接访问静态成员
- 实例成员: 实例成员就是在构造函数内部,通过this添加的成员。实例成员只能通过实例化的对象来访问。
- 静态成员: 在构造函数本身上添加的成员,只能通过构造函数来访问
❝可以得知new的主要作用是
❞
- new通过构造函数Star创建出来的实例可以访问到构造函数中的属性
- new通过构造函数Star创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过new操作符,实例与构造函数通过原型链连接了起来
new 的实现以及内部原理
- 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型
- 属性和方法被加入到 this 引用的对象中
- 新创建的对象由 this 所引用,并且最后隐式的返回 this
function _new(Fn) {
var obj = {}, //创建一个空的对象 和new Object()一个道理
Fn= [].shift.call(arguments);//剪切出参数一 也就是被new的函数
// 链接到原型,obj 可以访问到构造函数原型中的属性
obj.__proto__ = Fn.prototype;//绑定原型链 ❗继承原型方法
Fn.apply(obj, arguments);//将new构造函数的this❗静态方法 给到obj对象上
return obj;
};
❝ES6版本
❞
function _new(Fn, ...arg) {
let obj = Object.create(Fn.prototype)//原生优化 等同于 --> var obj = {}, obj.__proto__ = Fn.prototype
//Fn当前需要被new实例的类 ...arg 传参列表
Fn.call(obj, ...arg);//将new的this指向Fn的原型
return obj
}
//优化
function myNew(fn, ...args) {
let instance = Object.create(fn.prototype);
let result = fn.call(instance, ...args)
return typeof result === 'object' ? result : instance;
}
原型
❝原型对象
❞
- 在声明了一个函数之后,浏览器会自动按照一定的规则创建一个对象,这个对象就叫做原型对象(prototype)
- 声明函数后,这个构造函数(声明了的函数)中会有一个属性prototype,这个属性指向的就是这个构造函数对应的原型对象;
- 原型对象中有一个属性constructor,这个属性指向的是这个构造函数(new 函数对象) 原型上可以自定义方法也可以用原型上自带的方法;共享给继承改原型链子级自己的属性和方法
function A() {}
let a = new A()
A.prototype.x = 2
console.log(a.x)//1
__ proto__ 以及原型链
❝实例对象独有的,指向生成该对象的函数的原型
❞
- 主要作用是可以对对象属性的访问修改和删除,以及解决继承问题
「原型链」
- 原型与原型层层相链接的过程,当对象需要某个属性时,当前对象没有该属性时,就会查找它的原型是否有,一直递归原型对象
- __proto__是任何对象都有的,而且js万物皆对象,他们会连起来最终指向 null 空对象
ES6 Class
❝class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
❞
- 子类的__proto__属性,表示构造函数的继承,总是指向父类。
- 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}
class B extends A {
}
B.__proto__ === A //>> true
B.prototype.__proto__ === A.prototype //>> true
ES5和ES6的继承区别
ES6 class 内部所有定义的方法都是不可枚举的;
ES6 class 默认即是严格模式;
ES6的继承机制(extends )实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
ES5的继承是通过prototype或构造函数机制来实现。
ES6 class 子类必须在构造函数中调用super(),这样才有this对象;
ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。
继承
原型继承
function Parent2(name) {
this.name = name;
this.play = [1, 2, 3]
}
Parent2.prototype.played = function () {
return this.play;
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2('parent');
//子级无法传参给父级 如果没传parent会显示undefined
let obj = new Child2('childName')
console.log(obj.name)
//子级能够修改到父级属性和方法
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.played(), s2.played());
- 优点:
- 继承了父类的属性和方法,又继承了父类的原型对象
- 缺点
- 无法实现多继承(因为已经指定了原型对象了)
- 子类可以重写父类方法(这样会导致继承父类的其他实例也受到影响)
- 创建子类时,无法向父类构造函数传参数
call继承(构造继承)
function Parent(name, sex) {
this.name = name
this.sex = sex
this.colors = ['white', 'black']
}
Parent.prototype.a = function () { console.log(123) }
function Child(name, sex) {
Parent.call(this, name, sex)
}
//子级修改父级不会影响到
var child1 = new Child('child1', 'boy')
child1.colors.push('yellow')
console.log(child1)//Child { name: 'child1', sex: 'boy', colors: [ 'white', 'black', 'yellow' ] }
var child2 = new Child('child2', 'girl')
console.log(child2)//Child { name: 'child2', sex: 'girl', colors: [ 'white', 'black' ] }
//子级无法继承父级的原型属性和方法
child1.a()//child1.a is not a function
//子级
console.log(child1 instanceof Child)//是属于chiild实例的
console.log(child1 instanceof Parent)//但不是parent的实例
- 优点:
- 解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数
- 缺点:
- 只能继承父类实例的属性和方法,不能继承原型的;
- 实例并不是父类的实例,只是子类的实例
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
- 由于它是复制了父类构造函数中的属性和方法,这样每个子级都复制了各自属性和方法, 可是有的方法完全没有必要复制,可以用来共用的所以就说不能够「函数复用
组合继承 (前2个的组合)
function Father(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Father.prototype.sayName = function () {
console.log(this.name);
};
function Son(name, age) {
Father.call(this, name);//继承实例属性,第一次调用Father()
this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.constructor = Son; //让原型的构造器指向他
let obj = new Son('汤姆',18)
console.log(obj)
obj.sayName() // 使用父类的方法
- 实现方式:
- 使用原型链继承来保证子类能继承到父类原型中的属性和方法
- 使用构造继承来保证子类能继承到父类的实例属性和方法
- 优点:
- 可以继承父类实例属性和方法,也能够继承父类原型属性和方法 弥补了原型链继承中引用属性共享的问题
- 可传参,可复用
- 缺点:
- 使用组合继承时,父类构造函数会被调用两次,并且生成了两个实例, 子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。
原型式继承
ES6的Object.create()代替了它
function create (obj) {
var newObj = {}
newObj.__proto__ = obj
return newObj;
}
- 优点:
- 再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分。
- 缺点:
- 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类 谨慎定义方法,以免定义方法也继承对象原型的方法重名 无法传参
寄生继承(了解)
function createAnother(original){
var clone = object(original);//通过调用object函数创建一个新对象
clone.sayHi = function(){//以某种方式来增强这个对象
alert("hi");
};
return clone;//返回这个对象
}
- 缺点(同原型式继承)
寄生组合方法(了解)
function extend(subClass,superClass){
var prototype = object(superClass.prototype);//创建对象
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}
❝over 仅个人笔记收录
❞
本文使用 mdnice 排版