日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。
构造函数
用 new
关键字来调用的函数,称为 构造函数。
构造函数中一般有两个属性,一个是 原型上的属性,一个是 实例上的属性。
function Animal(name) {
this.name = name;
this.arr = [1, 2, 3]
}
let a1 = new Animal('小狗');
let a2 = new Animal('小猫');
console.log(a1.arr === a2.arr); // false
实例的属性指向不同的存储空间(堆内存),所以输出结果是 false
,也就是实例本身的属性。
通过定义原型上的属性,可以使实例拥有 原型上的属性
Animal.prototype.address = {location:'家里'}
console.log(a1.address === a2.address) // true
原型与原型链
首先我们要清楚 类(构造函数) 、原型 和 constructor 之间的关系。
- 每一个类(函数)都具有
prototype
,并且属性值是一个Object
- 对象上天生具有一个属性
constructor
,指向类本身 - 每一个对象(普通对象 、 prototype 、实例 、函数 等 )都具备
__proto__
function Foo() {...};
let f1 = new Foo();
__proto__
:当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__
属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__
属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null,真正的空值。
prototype
:包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的 prototype
对象。
constructor
:指向该对象的构造函数,每个对象都有构造函数。若对象本身不具备constructor
属性,则会通过__proto__
向原型链进行查找,找到原型链中constructor后,确定其指向,并进行继承。
关于 原型链查找机制,我个人是这么理解的。
首先实例的__proto__
会始终指向其构造函数的prototype
属性(f.__proto__ === Fn.prototype
),构造函数和其所有父类(Fn
、Function
、Object
)均指向Function.prototype
,Fn.prototype.__proto__
指向的是Object.prototype
,而Function.prototype.__proto__
指向Object.prototype
。Object.prototype.__proto__
指向null
,就是此原型链的终点。
关于原型链,可以将上图好好理解一下,这张图更直观的表述了prototype
、__proto__
和 constructor
之间的关系。没事的时候也可以将他们画一下。
类的继承
构造函数 其实 就是类 的一种。
function Animal(name) {
this.name = name;
this.eat = '吃肉';
}
Animal.prototype.address = {location: '山里'}
function Tiger(name) {
this.name = name;
this.age = 10;
}
Tiger.prototype.say = function () {
console.log('说话');
}
在这里我们模拟一个 父类 Animal
和一个子类 Tiger
-
继承父类实例上的属性
我们只需要子类上加一个
.call
改变一下 this 的指向即可function Tiger(name) { this.name = name; this.age = 10; Animal.call(this); //将父类的this指向子类,使子类继承父类中的属性。 } let tiger = new Tiger(); console.log(tiger.eat); // 吃肉
-
继承父类原型上的方法
上述做法我们只继承了父类实例上的属性,并没有继承其原型上的属性。
console.log(tiger.address); // undefined
这里我们有这么几种解决方案
-
父类.prototype.__proto__ = 子类.prototype
我们的子类和父类的
prototype
分别指向不同的方法和对象。所以我们为了使子类的原型继承父类原型上的方法,可以让子类的prototype.__proto__
指向父类的prototype
。Tiger.prototype.__proto__ = Animal.prototype; console.log(tiger.address); // {location: '山里'}
这样我们就实现了其中一种继承方法。
-
Object.create
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。Tiger.prototype = Object.create(Animal.prototype); console.log(tiger.address); // {location: '山里'}
-
Object.setPrototypeOf
使用
Object.setPrototypeOf()
方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或null
。Object.setPrototypeOf(Tiger.prototype, Animal.prototype);
(注:以上方法需要在子类的原型方法绑定前添加。)
这种方法我们无法向父类传参,只能给子类传参。
-
ES6中的类(class)
首先,ES6与ES5类的实现思路相同,同样是利用 原型链 来进行实现的。
class Animal {
constructor(name) {
this.name = name;
this.eat = '吃肉';
}
say() {
console.log('say');
}
}
上面就是最简单的一种类的实现方式。
但是 ES6的类 和ES5有几点不同
-
类不可以被当做函数调用。
Animal() // Class constructor Animal cannot be invoked without 'new'
ES6的类需要使用new作为关键字来进行实例化
同理,如果我们调用原型上的方法,可以将类实例化出来后,直接进行调用。
let a = new Animal(); a.__proto__.say(); // say
(注:ES6规范中,若单独调用原型上的方法,this是不存在的)
比如我们直接将原型上的方法实例化出来
// 暂时修改一下Animal类上的say()方法,测试完后再将此方法改回 say() { console.log(this); }; let say = a.__proto__.say; say(); // undefined
返回的结果就是
undefined
-
包含静态方法(ES7中的静态属性)
ES6中允许类存在 私有方法(ES7中的私有属性)
class Animal { // ES6 static flag() { return 'test'; } // ES7 static flag = 'test' } console.log(Animal.flag()) // test
(注:在ES6的环境中,不能使用ES7的写法)
调用时,需要直接用类来进行调用,实例不能进行调用。
-
使用
extends
关键字实现继承extends
可以直接实现继承。class Tiger extends Animal {} let t = new Tiger('老虎') t.say(); // say console.log(t.eat); // 吃肉 console.log(t.name); // 老虎
首先,父类的方法与原型 直接继承给了子类。
随后,传递的值 老虎 ,被直接传递给了父类的
constructor
,直接输出了结果。class Tiger extends Animal { constructor(){ } // 报错 }
这里我们还有一个需要注意的,父类存在自己的
constructor
,子类不能再定义constructor
如果想要实现此功能,需要使用关键字
super
。class Tiger extends Animal { constructor(name){ // Animal.call(this, name) super(name) } }
(提示:我们可以去 babel 上查看ES6中class的实现方式,加深对class的理解)
本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
您也可以关注我的 个人站点、博客园 和 掘金,我会在文章产出后同步上传到这些平台上。
最后感谢您的支持!