框架总览
😁 前言
😁 Javascript中new()到底做了些什么?
😁 ES 5创建对象的几种方式
- 🏆 通过{}创建对象
- 🏆 通过new Object()创建对象
- 🏆 使用字面量创建对象
- 🏆 使用工厂模式创建对象
- 🏆 通过原型+构造函数的方式创建对象
😁 ES 5实现继承的几种方式
- 🏆 原型链继承
- 🏆 借用构造函数
- 🏆 组合继承
- 🏆 原型式继承 (值类型继承)
- 🏆 寄生式继承
- 🏆 寄生组合式继承
😁 ES 5 的构造函数到 ES 6 的类
- 🏆 Class的基本语法
- 🏆 静态方法
- 🏆 实例属性的新写法
😁 new构造器和Object.create()创建对象的区别
😁 ES6中箭头函数VS普通函数的this指向
- 🏆 普通函数中this
- 🏆 ES6箭头函数中this
😁 ES6的继承原理
- 🏆 父类的static方法,也会被子类继承
- 🏆 Object.propotypeOf()
- 🏆 super关键字
- 🏆 类的prototype属性和proto属性
😁 ES6 class与ES5 function区别
前言
虽然 ES 6 里引入了「类」这个概念,但是实际上 ES 6 的类只是 ES 5 的语法糖,本质上还是一个「构造函数」。本片笔记主要回顾 ES 5 通过构造函数创建对象的一般方法,以及 ES 5 中类的继承的一般方法;然后再介绍 ES 6 的写法。
Javascript中new()到底做了些什么?
和其他高级语言一样 javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象。 但在 javascript 中,万物皆对象,为什么还要通过 new 来产生对象? 本节将带你一起来探索 javascript 中 new 的奥秘...
假如有一个Base构造函数,要创建 Base 的新实例,必须使用 new 操作符。
以这种方式调用构造函数实际上会经历以下 4个步骤:
- (1) 创建一个新对象;
- (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
- (3) 执行构造函数中的代码(为这个新对象添加属性) ;
- (4) 返回新对象。
new 操作符
基于上面的例子,我们执行如下代码
var obj = new Base();
这样代码的结果是什么,我们在Javascript引擎中看到的对象模型是:
new操作符具体干了什么呢?其实很简单,就干了三件事情。
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
第一行,我们创建了一个空对象obj
第二行,我们将这个空对象的proto成员指向了Base函数对象prototype成员对象
第三行,我们将Base函数对象的this指针替换成obj,然后再调用Base函数,于是我们就给obj对象赋值了一个id成员变量,这个成员变量的值是”base”,关于call函数的用法。
如果我们给Base.prototype的对象添加一些函数会有什么效果呢?
例如代码如下:
Base.prototype.toString = function() {
return this.id;
}
那么当我们使用new创建一个新对象的时候,根据proto的特性,toString这个方法也可以做新对象的方法被访问到。于是我们看到了:
ES 5 创建对象的几种方式
构造函数、原型对象和实例之间的关系
在讲创建对象的方式之前。复习一下构造函数、原型对象和实例之间的关系。 代码表示:
function F(){}
var f = new F();
// 构造器
F.prototype.constructor === F; // true
F.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 实例
f.__proto__ === F.prototype; // true
F.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
复制代码
画了一张图表示:
js常用的几种创建对象的方式有:
1、通过{}
创建对象
<script>
'use strict'; //使用strict模式
/**
使用{}创建对象,等同于 new Object();
**/
var o = {};
o.name = 'jack';
o.age = 20;
o.sayName = function(){
alert(this.name);
}
</script>
如果对象不用重复创建,这种方式是比较方便的。
2、通过new Object()
创建对象
<script>
'use strict';
// 使用 new Object() 创建对象
var o = new Object();
o.name = "zhangsna";
o.sayName = function(){
alert(this.name);
}
</script>
3、使用字面量创建对象
对象字面变量是对象定义的一种简写形式,举个例子:
var person = {name: 'zhang', age:20}, // 这就是字面量形式,完全等价于`var person = {};
小结:前面三种创建对象的方式存在2个问题:1.代码冗余; 2.对象中的方法不能共享,每个对象中的方法都是独立的。
4、使用工厂模式创建对象
这种方式是使用一个函数来创建对象,减少重复代码,解决了前面三种方式的代码冗余的问题,但是方法不能共享的问题还是存在。
<script>
'use strict';
// 使用工厂模式创建对象
// 定义一个工厂方法
function createObject(name){
var o = new Object();
o.name = name;
o.sayName = function(){
alert(this.name);
};
return o;
}
var o1 = createObject('zhang');
var o2 = createObject('li');
//缺点:调用的还是不同的方法
//优点:解决了前面的代码重复的问题
alert(o1.sayName===o2.sayName);//false
</script>
5、通过原型+构造函数的方式创建对象
此部分涉及的内容基本出自《JavaScript 高级程序设计(第 3 版)》第六章:「面向对象的程序设计」,里面介绍了不少种创建对象的方法和实现继承的方法,这里我只从这些方法里各挑出一种最为广泛使用的方法。首先是书上所谓的「组合使用构造函数模式和原型模式」的创建对象的方法:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
};
}
Person.prototype.sayAge = function() {
alert(this.age)
}
var p1 = new Person('zhang',18);
var p2 = new Person('li',17);
p1.sayName();
p2.sayName();
alert(p1.constructor === p2.constructor);//true
alert(p1.constructor === Person);//true
alert(typeof(p1));//object
alert(p1 instanceof Object); //true
alert(p2 instanceof Object); //trueb
alert(p1.sayName===p2.sayName);//false
内存模型:
上面就是一个「类」:每个实例都拥有的各自不同的属性放到构造函数内部,通过 this
来初始化;所有实例都共享的方法放到构造函数的 prototype
上;同时这个类本身还有一些只是自己所有的属性或方法,别的面向对象语言里称之为静态属性(方法),这些静态属性直接挂到构造函数本身上。通过 new
操作符执行这个函数就可以得到对应的实例对象,所有的对象都有着自己的 name
、age
属性,同时共享原型对象上的 sayAge
方法。上面就是 ES 5 里一个类的基本创建方法
ES 5 实现继承的几种方式
在ES5继承的实现非常有趣的,由于没有传统面向对象类的概念,Javascript利用原型链的特性来实现继承,这其中有很多的属性指向和需要注意的地方。
原型链的特点和实现就是通过将子类构造函数的原型作为父类构造函数的实例,这样就连通了子类-子类原型-父类,原型链的特点就是逐层查找,从子类开始一直往上直到所有对象的原型Object.prototype,找到属性方法之后就会停止查找,所以下层的属性方法会覆盖上层。
1.原型链继承
一个基本的基于原型链的继承过程大概是这样的:
//先来个父类,带些属性
function Parent1() {
this.name = ['super1']
this.reName = function () {
this.name.push('super111')
}
}
function Child1() {
}
Child1.prototype = new Parent1()
var child11 = new Child1()
var child12 = new Child1()
var parent1 = new Parent1()
child11.reName()
console.log(child11.name, child12.name) // [ 'super1', 'super111' ] [ 'super1', 'super111' ], 可以看到子类的实例属性皆来自于父类的一个实例,即子类共享了同一个实例
console.log(child11.reName === child12.reName) // true, 共享了父类的方法
原型链实现的继承主要有几个问题:
- 1、本来我们为了构造函数属性的封装私有性,方法的复用性,提倡将属性声明在构造函数内,而将方法绑定在原型对象上,但是现在子类的原型是父类的一个实例,自然父类的属性就变成子类原型的属性了;
这就会带来一个问题,子类实例共享属性,造成实例间的属性会相互影响。这就违背了我们想要属性私有化的初衷; - 2、创建子类的实例时,不能向父类的构造函数传递参数
function Super(){
this.flag = true;
}
Super.prototype.index = 1;
function Sub(){
this.subFlag = false;
}
Sub.prototype = new Super();
var obj = new Sub();
obj.index = 2; //修改之后,由于是原型上的属性,之后创建的所有实例都会受到影响
var obj_2 = new Sub();
console.log(obj.index) // 2;
2.借用构造函数
为了解决以上两个问题,有一个叫借用构造函数的方法
只需要在子类构造函数内部使用apply或者call来调用父类的函数即可在实现属性继承的同时,又能传递参数,又能让实例不互相影响
function Super(){
this.flag = true;
}
function Sub(){
Super.call(this) //如果父类可以需要接收参数,这里也可以直接传递
}
var obj = new Sub();
obj.flag = flase;
var obj_2 = new Sub();
console.log(obj.flag) //依然是true,不会相互影响
缺点:不可以使用父类原型上的属性和方法
3. 组合继承
结合借用构造函数和原型链的方法,可以实现比较完美的继承方法,可以称为组合继承:
function Parent3() {
this.name = ['super3']
}
Parent3.prototype.reName = function() {
this.name.push('super31')
}
function Child3() {
Parent3.call(this) // 生成子类的实例属性(但是不包括父对象的方法)
}
Child3.prototype = new Parent3() // 继承父类的属性和方法(副作用: 父类的构造函数被调用的多次,且属性也存在两份造成了内存浪费)
var child31 = new Child3()
var child32 = new Child3()
child31.reName()
console.log(child31.name, child32.name) // [ 'super3', 'super31' ] [ 'super3' ], 子类实例不会相互影响
console.log(child31.reName === child32.reName) //true, 共享了父类的方法
这里还有个小问题,Child3.prototype = new Parent3()
会导致Child3.prototype
的constructor指向Parent3;
然而constructor
的定义是要指向原型属性对应的构造函数的,Child3.prototype
是Child3
构造函数的原型,所以应该添加一句纠正:
Child3.prototype.constructor = Child3 ;
优点:融合原型链继承和构造函数的优点,是JavaScript中最常用的继承模式
缺点:调用了两次父类构造函数,子类实例的属性存在两份。造成内存浪费
4.原型式继承 (值类型继承)
这种方法并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型,如下函数:
// 原型式继承
function CreateObj(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name: 'xiaopao',
friend: ['daisy', 'kelly']
}
var person1 = CreateObj(person);
// var person2 = CreateObj(person);
person1.name = 'person1';
// console.log(person2.name); // xiaopao
person1.friend.push('taylor');
// console.log(person2.friend); // ["daisy", "kelly", "taylor"]
// console.log(person); // {name: "xiaopao", friend: Array(3)}
person1.friend = ['lulu'];
// console.log(person1.friend); // ["lulu"]
// console.log(person.friend); // ["daisy", "kelly", "taylor"]
// 注意: 这里修改了person1.name的值,person2.name的值并未改变,并不是因为person1和person2有独立的name值,而是person1.name='person1'是给person1添加了name值,并非修改了原型上的name值
// 因为我们找对象上的属性时,总是先找实例上对象,没有找到的话再去原型对象上的属性。实例对象和原型对象上如果有同名属性,总是先取实例对象上的值
在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的 原型,后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
举例:
ECMAScript 5通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象(object()中的参数o)和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()与 object()方法的行为相同。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"],
sayname: function() {
console.log(this.name);
}
};
var yetAnotherPerson = Object.create(person);
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
//相当于:
//var anotherPerson = Object.create(person);
//anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
anotherPerson.sayname(); //"Greg"
优点:
- 不用再执行和建立person的实例
缺点: - 从例子中可以看出,同原型链继承一样,引用类型值的属性friends属性是被共享的,这不是我们期待的
5.寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
原理:二次封装原型式继承,并拓展
在主要考虑对象而不是自定义类型或构造函数的情况下使用,在原型式继承的思路上增强了对象(给子对象添加属性)
function createAnother (original) {
// 克隆一个新对象
var clone = Object.create(original)
// 给新对象添加一个方法,增强属性
clone.sayHi = function() {
console.log('Hi')
}
return clone
}
var person = {
name: 'xiaopao',
friend: ['daisy', 'kelly']
}
var person1 = createAnother(person);
var person2 = createAnother(person);
person1.name = 'person1';
console.log(person2.name); // xiaopao
person1.friend.push('taylor');
console.log(person2.friend); // ["daisy", "kelly", "taylor"]
console.log(person); // {name: "xiaopao", friend: Array(3)}
person1.friend = ['lulu'];
console.log(person1.friend); // ["lulu"]
console.log(person.friend); // ["daisy", "kelly", "taylor"]
缺点:每次创建对象都会执行创建对象的方法
6.寄生组合式继承
子类构造函数复制父类的自身属性和方法,子类原型只接收父类的原型属性和方法
所谓寄生组合继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
其背后的基本思路是:不必为了指定子类型的原型而调用父类型的构造函数,我们所需要的无非就是父类型的原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给予类型的原型。
// 寄生组合式继承
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child('kevin', '18');
console.log(child1)
组合继承最大的缺点是会调用两次父构造函数。
一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18');
回想下 new 的模拟实现,其实在这句中,我们会执行:
Parent.call(this, name);
在这里,我们又会调用了一次 Parent 构造函数。
所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为colors,属性值为['red', 'blue', 'green']。
那么我们该如何精益求精,避免这一次重复调用呢?
如果我们不使用 Child.prototype = new Parent()
,而是间接的让Child.prototype
访问到 Parent.prototype
呢?
最后我们封装一下这个继承方法:
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);
var child1 = new Child('kevin', '18');
console.log(child1);
优点: 这种方式的高效率体现它只调用了一次Parent构造函数,并且因此避免了再Parent.prototype上面创建不必要的,多余的属性。普遍认为寄生组合式继承是引用类型最理想的继承方式
JavaScript的继承一共有6种方式。可以根据不同的需求使用。
1.仅仅考虑创建相似对象的情况下,继承值类型,建议使用原型式继承;还需要继承引用类型,建议使用寄生式继承
2.继承引用类型的完整继承范式应该是寄生组合式继承,因为相比组合继承寄生组合式继承比较高效(减少调用父类型构造函数的次数)
ES 5 的构造函数到 ES 6 的类
看完ES5的实现,再来看看ES6的继承实现方法,其内部其实也是ES5组合继承的方式,通过call借用构造函数,在A类构造函数中调用相关属性,再用原型链的连接实现方法的继承
1.Class的基本语法
1.简介
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已.
来个例子对比一下:
ES6之前,生成实例对象的传统方法是通过构造函数。
function Point(x, y) {
this.x =x;
this.y=y;
}
Point.prototype.toString = function() {
return '('+this.x+','+this.y+')';
}
var p = new Point(1,2);
上面的代码用ES6改写:
class Point {
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return '('+this.x+','+this.y+')';
}
}
var p = new Point(1,2);
上面ES6改写的代码注意看两点:
1.ES6写法定义了一个类,可以看到Point类里面有个constructor方法(构造方法)----ES5的构造函数Point,就对应的是ES6的Point类的构造方法
2.Point类中我们还看到了toString方法。注意,定义类的方法的时候,在方法前不要加function关键字,直接把函数定义放进去即可。还需要注意一点,方法之间是不能用逗号分隔的,否则会报错。
2.静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
注意,如果静态方法包含this关键字,这个this指的是类,而不是实例
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
既然类的静态方法不可被实例所继承,但是却可以被子类继承,不赘述。
3.实例属性的新写法
实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。写法对比:
//实例属性this._count定义在constructor()方法里面
class IncreasingCounter {
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
//属性定义在类的最顶层,其它不变
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。
new构造器和Object.create()创建对象的区别
new
new func()主要过程如下:
1.创建一个空对象obj;
2.将该空对象的原型设置为构造函数的原型,即obj.proto = func.prototype;
3.以该对象为上下文执行构造函数,即func.call(obj);
4.返回该对象,即return obj。
对于第3、4步还有个小细节,如果第3步func有返回值且返回值为对象,则第4步会返回func的返回值,反之则默认返回obj。
模仿new原理的代码如下:
function new(func) {
var child = Object.create(func.prototype);
func.call(child);
return child;
}
Object.create(prototype, descriptors)
参数 | 描述 |
---|---|
prototype(必需) | 要用作原型的对象,可以为 null |
descriptors(可选) | 包含一个或多个属性描述符的 JavaScript 对象 |
返回值
一个具有指定的内部原型且包含指定的属性(如果有)的对象;
在模仿new原理的代码中用到了Object.create(),它的作用是以入参为原型创建一个空对象,即
Object.create = function (obj) {
return { '__proto__': obj};
};
或
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
new和Object.create区别
function Person(name) {
this.name = 1
}
Person.prototype.name = 2;
//生成实例
var Person_new = new Person();
var Person_create = Object.create(Person);
var Person_create_prototype = Object.create(Person.prototype);
console.log(Person_new, Person_create, Person_create_prototype);
// Person { name: 1 } Function {} Person {}
区别
参数 | 描述 | 参数 | 描述 |
---|---|---|---|
属性 | new构造函数 | Object.create(构造函数) | Object.create(构造函数原型) |
实例类型 | 实例对象 | 函数 | 实例对象 |
实例name | 1 | 无 | 无 |
原型name | 2 | 无 | 2 |
小结
参数 | 描述 | 参数 |
---|---|---|
对比 | new | Object.create |
使用目标 | 函数 | 函数和对象 |
返回实例 | 实例对象 | 函数和实例对象 |
实例属性 | 继承构造函数属性 | 不继承构造函数属性 |
原型链指向 | 构造函数原型 | 构造函数/对象本身 |
ES6中箭头函数VS普通函数的this指向
很多在撸代码的时候,涉及到this总会出现一些问题,无法得到我们想要的值。大多数时候是我们没有弄清楚this的指向到底是什么,所以在某些情况下,this得到的不是我们想要的值。最近学习了一下函数中this指向的问题,在此分享出来也方便自己日后巩固学习。
普通函数与ES6中箭头函数里,this指向的问题
1、普通函数中this
(1)总是代表着它的直接调用者,如obj.fn,fn里的最外层this就是指向obj
(2)默认情况下,没有直接调用者,this指向window
(3)严格模式下(设置了'use strict'),this为undefined
(4)当使用call,apply,bind(ES5新增)绑定的,this指向绑定对象
注释:
-
call
方法第一个参数是this的指向,后面传入的是一个参数列表。当第一个参数为null、undefined的时候,默认指向window。如:getColor.call(obj, 'yellow', 'blue', 'red')
2.apply
方法接受两个参数,第一个参数是this的指向,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window。如:getColor.apply(obj, ['yellow', 'blue', 'red'])
bind
方法和 call 方法很相似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。区别在于bind方法返回值是函数以及bind接收的参数列表的使用。低版本浏览器没有该方法,需要自己手动实现以上
call
,apply
,bind
方法是ES5新增,如果想要了解更多可以自行百度谷歌研究一下(▽)
2、ES6箭头函数中this
(1)默认指向定义它时,所处上下文的对象的this指向。即ES6箭头函数里this的指向就是上下文里对象this指向,偶尔没有上下文对象,this就指向window
(2)即使是call,apply,bind等方法也不能改变箭头函数this的指向
一些实例加深印象
(1)hello是全局函数,没有直接调用它的对象,也没有使用严格模式,this指向window
function hello() {
console.log(this); // window
}
hello();
(2)hello是全局函数,没有直接调用它的对象,但指定了严格模式('use strict'),this指向undefined
function hello() {
'use strict';
console.log(this); // undefined
}
hello();
(3)hello直接调用者是obj,第一个this指向obj,setTimeout里匿名函数没有直接调用者,this指向window
const obj = {
num: 10,
hello: function () {
console.log(this); // obj
setTimeout(function () {
console.log(this); // window
});
}
}
obj.hello();
(4)hello直接调用者是obj,第一个this指向obj,setTimeout箭头函数,this指向最近的函数的this指向,即也是obj
const obj = {
num: 10,
hello: function () {
console.log(this); // obj
setTimeout(() => {
console.log(this); // obj
});
}
}
obj.hello();
(5)箭头函数的this定义:箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。ES5中this应该指向上下文函数this的指向
,ES6的class中箭头函数永远指向class.简单的说,函数在定义时,this就继承了定义函数的对象。
这里上下文没有函数对象,就默认为window,而window里面没有radius这个属性,就返回为NaN。
const obj = {
radius: 10,
diameter() {
return this.radius * 2
},
perimeter: () => 2 * Math.PI * this.radius
}
console.log(obj.diameter()) // 20
console.log(obj.perimeter()) // NaN
看下面代码:
打印的结果:
以上就是ES6箭头函数与普通函数里,this的指向区别。相信在项目开发过程中,会有一定帮助,避免陷入坑里耽搁项目进程。
Es 6 的继承原理
1.简介
Class 可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。extends本身就是和ES5寄生组合式继承的效果是一样的。
ES6 extends
继承做了什么操作
我们先看看这段包含静态方法的ES6
继承代码:
// ES6
class Parent{
constructor(name){
this.name = name;
}
static sayHello(){
console.log('hello');
}
sayName(){
console.log('my name is ' + this.name);
return this.name;
}
}
class Child extends Parent{
constructor(name, age){
super(name);
this.age = age;
}
sayAge(){
console.log('my age is ' + this.age);
return this.age;
}
}
let parent = new Parent('Parent');
let child = new Child('Child', 18);
console.log('parent: ', parent); // parent: Parent {name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child: Child {name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18
复制代码
其中这段代码里有两条原型链,不信看具体代码。
// 1、构造器原型链
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2、实例原型链
child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
复制代码
一图胜千言,笔者也画了一张图表示,如图所示:
结合代码和图可以知道。 ES6 extends
继承,主要就是:
- 把子类构造函数(
Child
)的原型(__proto__
)指向了父类构造函数(Parent
),
- 把子类构造函数(
- 把子类实例
child
的原型对象(Child.prototype
) 的原型(__proto__
)指向了父类parent
的原型对象(Parent.prototype
)。
- 把子类实例
这两点也就是图中用不同颜色标记的两条线。
- 子类构造函数
Child
继承了父类构造函数Preant
的里的属性。使用super
调用的(ES5
则用call
或者apply
调用传参)。 也就是图中用不同颜色标记的两条线。
- 子类构造函数
ES6的extends的ES5版本实现
知道了ES6 extends继承做了什么操作和设置proto的知识点后,把上面ES6例子的用ES5就比较容易实现了,也就是说实现寄生组合式继承,简版代码就是:
// ES5 实现ES6 extends的例子
function Parent(name){
this.name = name;
}
Parent.sayHello = function(){
console.log('hello');
}
Parent.prototype.sayName = function(){
console.log('my name is ' + this.name);
return this.name;
}
function Child(name, age){
// 相当于super
Parent.call(this, name);
this.age = age;
}
// new
function object(proto){
function F() {}
F.prototype = proto;
return new F();
}
function _inherits(Child, Parent){
// Object.create
Child.prototype = object(Parent.prototype);
// __proto__
// Child.prototype.__proto__ = Parent.prototype;
Child.prototype.constructor = Child;
// ES6
// Object.setPrototypeOf(Child, Parent);
// __proto__
Child.__proto__ = Parent;
}
_inherits(Child, Parent);
Child.prototype.sayAge = function(){
console.log('my age is ' + this.age);
return this.age;
}
var parent = new Parent('Parent');
var child = new Child('Child', 18);
console.log('parent: ', parent); // parent: Parent {name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child: Child {name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18
1、constructor
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
constructor 方法是类的构造函数,是一个默认方法,通过 new 命令创建对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个默认的 consructor 方法会被默认添加。所以即使你没有添加构造函数,也是会有一个默认的构造函数的。一般 constructor 方法返回实例对象 this ,但是也可以指定 constructor 方法返回一个全新的对象,让返回的实例对象不是该类的实例。
2、父类的static方法,也会被子类继承
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
var C = new B();
C.hello(); // C.hello is not a function
B.hello() // hello world
上面代码中,hello()是A类的静态方法,B继承A,也继承了A的静态方法。所以B.hello()能正常执行。
但是A,B的实例无法继承A的静态方法。所以不可执行
3、Object.propotypeOf()
Object.propotypeOf方法可以用来从子类上获取父类。
Object.propotypeOf(colorPoint) === Point
//true
因此,使用这个方法判断,一个类是否继承了另一个类,
4、super关键字
子类必须在constructor方法中调用super
方法,否则新建实例时会报错。
这是因为子类自己的this对象,必须通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后对其加工,加上子类自己的实例属性和方法,如果不调用super方法,子类就得不到this对象。
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同.
第一种情况,
super
作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
第二种情况,
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
5、类的prototype属性和 _proto _ 属性
函数的_ proto 为
f()
,对象的 proto _为Object
当我们在定义一个
class
(class的本质也是function,typeof class = function
)或者function构造函数的时候,操作系统会为其分配一块内存,用来存储一个对象,这个对象上保存着创建的每一个实例共享的属性和方法,这个对象叫做原型对象。prototype
为构造函数上的一个属性。是一个指针,指向原型对象。
当构造函数创建一个实例的时候,实例对象上也会有一个属性,
_ prop _
(null除外),这个属性也是指针。指向相应的构造函数的prototype属性。
以下三点需要谨记
1.每个对象都具有一个名为proto的属性;
2.每个构造函数(构造函数标准为大写开头,如Function(),Object()等等JS中自带的构造函数,以及自己创建的)都具有一个名为prototype的方法(注意:既然是方法,那么就是一个对象(JS中函数同样是对象),所以prototype同样带有proto属性);
3.每个对象的proto属性指向自身构造函数的prototype;
需要注意的指向是
1.Function的proto指向其构造函数Function的prototype;
2.Object作为一个构造函数(是一个函数对象!!函数对象!!),所以他的proto指向Function.prototype;
3.Function.prototype的proto指向其构造函数Object的prototype;
4.Object.prototype的prototype指向null(尽头);
关于prototype属性和 _proto _的区别可以看这篇文章__ proto __和prototype
ES6 class与ES5 function区别
1.ES6中类的prototype上所有定义的方法,都是不可枚举的(non-enumerable)
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
但是ES5中是可以枚举的:
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
2. constructor 方法
区别1:
- 在function定义的构造函数中,其prototype.constructor属性指向构造器自身
- 在class定义的类中,constructor其实也相当于定义在prototype属性上的一个方法
区别2:
- ES5中构造函数可以直接调用,不一定非要使用new关键字
- ES6中构造函数的执行必须要new关键字,然后class的constructor函数默认自动执行
3.this指向
类的方法内部如果含有this,默认指向类的实例。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
上面代码中,printName
方法中的this
,默认指向Logger
类的实例。但是,如果将这个方法提取出来单独使用,this
会指向该方法运行时所在的环境-window(由于class
内部是严格模式,所以this
实际指向的是undefined),从而导致找不到print方法而报错。
可以在构造方法中绑定this来解决:
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
还可以使用箭头函数来解决:
class Obj {
constructor() {
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myObj // true
es6 class中,箭头函数内部的this总是指向定义时所在的class
。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。
4. 与ES5不同,类不存在变量提升
ES 5没有报错
var people = new person();
function person(){
this.name ='xiaoming'
}
ES 6下报错
var people = new person();
class person {
constructor(){
this.name = 'xiaoming'
}
}
5.继承方式不同
继承:一个对象直接使用另一个对象的属性和方法
- 在ES5的继承中,先创建子类的实例对象this,然后再将父类的方法添加到this上( Parent.apply(this) )。
- ES6采用的是先创建父类的实例this,完后再用子类的构造函数修改this(故要先调用 super( )方法获取的this)
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString();
}
}
ES6 中的继承使用到了 extends
关键字,function
也变成了 class
关键字。class
的本质还是一个语法糖,这个大家都会脱口而出,但是在继承机制这里到底是如何做到的,我们看一下 babel
在此处是如何帮我们转译的,
var ColorPoint = function(_Point) {
_inherits(ColorPoint, _Point);
function ColorPoint(x, y, color) {
var _this;
_classCallCheck(this, ColorPoint);
_this = _possibleConstructorReturn(this, _getPrototypeOf(ColorPoint).call(this, x, y)); // 调用父类的constructor(x, y)
_this.color = color;
return _this;
}
_createClass(ColorPoint, [{
key: "toString",
value: function toString() {
return this.color + ' ' + _get(_getPrototypeOf(ColorPoint.prototype), "toString", this).call(this);
}
}]);
return ColorPoint;
}(Point);
如上是经过babel转译后的代码,有几个关键点:
一、 _inherits()
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
首先完成extends对象的校验,必须是function 或者null,否则报错。其次完成以下事情:
ColorPoint.__proto__ === Point;
ColorPoint.prototype.__proto__ === Point.prototype;
二、 ColorPoint 构造函数中 _classCallCheck(), _possibleConstructorReturn()
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
主要是用来检测构造函数不能直接调用,必须是通过new的方式来调用。
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
调用父类的构造函数,初始化一些实例属性,并将this返回。使用该返回的this赋值给子类的this对象,子类通过这一步返回的this对象,再该基础之上在添加一些实例属性。
这就是最大的不同之处。如果不经历这一步,子类没有this对象,一旦操作一个不存在的this对象就会报错。
三、 _createClass()
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
最后一步完成原型属性与静态属性的挂载,如果是原型属性,挂在在Constructor上的prototype上,如果是静态属性或者静态方法,则挂在Constuctor 上。
关于javaScript的对象的知识远不止这些,,只希望可以让新学js的小伙伴不那么盲目的去刻意记一些东西,当然学习最好的办法还是要多写,最简单的就是直接打开浏览器的控制台,去验证自己各种奇奇怪怪的想法,动起来吧~