原型和原型链
1.理解原型设计模式以及JavaScript中的原型规则
-
所有的引用类型(数组、对象、函数),都具有对象特征,即可自由扩展属性。
-
所有的引用类型,都有一个属性下划线__proto__`属性(隐式原型),属性值是一个普通对象;
-
所有函数,都具有一个prototype(显示原型),属性值是只有属性
constructor的对象,属性constructor指向函数自身; -
所有的引用类型(数组、对象、函数),其隐式原型指向其各自的原型对象;
-
当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的
__proto__属性(即它的构造函数的prototype)中去寻找;// 惯例,构造函数应以大写字母开头 function Person(name) { // 函数内this指向构造的对象 // 构造一个name属性 this.name = name // 构造一个sayName方法 this.sayName = function() { console.log(this.name) } } Person.prototype.drink = function() { console.log('喝东西') } // 使用自定义构造函数Person创建对象 let personA = new Person('张三') personA.__proto__.eat = function() { console.log('吃东西') } let personB = new Person('李四') personB.eat() // 输出:吃东西 personB.drink() // 输出:喝东西总结一下:
- 对象有
__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,指向其原型。 - 只有函数有
prototype属性,只有函数有prototype属性,只有函数有prototype属性,指向new操作符加调用该函数创建的对象实例的原型对象。
- 对象有
2.instanceof的底层实现原理,手动实现一个instanceof
instanceof的作用:用于检测右侧构造函数的原型是否存在于左侧对象的原型链上。
const isObj = obj => ((typeof obj === 'object') || (typeof obj === 'function')) && obj !== null
function myInstanceOf(instance,Ctor){
if (!isObj(Ctor)) // 右侧必须为对象
throw new TypeError('Right-hand side of 'instanceof' is not an object')
const instOfHandler = Ctor[Symbol.hasInstance]
// 右侧有[Symbol.hasInstance]方法,则返回其执行结果
if (typeof instOfHandler === 'function') return instOfHandler(instance)
// 右侧无[Symbol.hasInstance]方法且不是函数的,返回false
if (typeof Ctor !== 'function') return false
// 左侧实例不是对象类型,返回false
if (!isObj(instance)) return false
// 右侧函数必须有原型
const rightP = Ctor.prototype
if (!isObj(rightP))
throw new TypeError(`Function has non-object prototype '${String(rightP)}' in instanceof check`)
// 在实例原型连上查找是否有Ctor原型,有则返回true
// 知道找到原型链顶级还没有,则返回false
while (instance !== null) {
instance = Object.getPrototypeOf(instance)
if (instance === null) return false
if (instance === rightP) return true
}
}
3.实现继承的几种方式和他们的优缺点
原型链继承:一句话用父类实例作为子类原型
function parent1{
this.name=‘parent1’
this.play=[1,2]
}
function child1(){
this.type=‘child1’
}
child1.prototype=new parent1()
var ch1 = new child1()
优点:简单、可服用
缺点:无法传参、单一、共享父类属性改了就全变。
构造器继承:借用call让父类构造函数来增强子类实例
function parent2(){
this.name=‘parent2’
this.play=[1,2]
}
parent2.prototype.getName=function(){
reutrn this.name
}
function child2(){
parent2.call(this)
this.type=‘child2’
}
child2.prototype.constructor=parent2
优点:可传参、可继承多个父、解决原型的引用篡改问题。
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用
- 每个子类都有父类实例函数的副本,影响性能
组合继承
优点:结合了两种模式的优点,传参和复用
缺点:调用了两次父类构造函数(耗内存),属性和方法也是生成两份,子类的构造函数会代替原型上的那个父类构造函数。
function parent3(){
this.name=‘parent3’
this.play=[1,2]
}
parent3.prototype.getName =function(){
reutrn this.name
}
function child3(){
parent3.call(this)
this.type=“child3”
}
child3.prototype = new parent3()
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
原型继承
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
var chid4 = Object.create(parent4)
优点:类似于复制一个对象,用函数来包装。、
缺点:1、所有实例都会继承原型上的属性。 2、无法实现复用。(新实例属性都是后面添加的)
寄生继承
let parent5 = {
name: "parent5”,
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
//寄生就是在原型础上创建一个克隆函数然后把相关的方法在写一下
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
};
优点:没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点:没用到原型,无法复用。
寄生组合继承
//组合
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child6';
}
//寄生继承
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6);
console.log(person6.getName());
console.log(person6.getFriends());
优点:比较完美的实现了继承,解决了组合继承的问题
4.至少说出一种开元项目中应用原型继承的案例:
Jquery 、vue
5.描述new一个对象的详细过程,手动实现一个new操作符
过程:
1.创建新对象
2.设置原型,将对象的原型设置为函数的prototype对象。
3.让函数的this指向新创建的对象,执行构造函数。
4.判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function myNew(constructor, ...args) {
// 1. 创建一个新对象
const obj = {};
// 2. 为新对象添加属性__proto__,将该属性链接至构造函数的原型对象
obj.__proto__ = constructor.prototype;
// 3. 执行构造函数,this被绑定在新对象上
const res = constructor.call(obj, ...args);
// 4. 确保返回一个对象
return res instanceof Object ? res : obj;
}
6.理解es6 class和它的构造以及继承的底层实现原理
class的由来:
JS面向对象是通过构造函数实现的,这和其它语言差异很大。es6引入了class这个概念作为对象的模版,解决了这个差异。
constructor
ES6 的类,完全可以看作构造函数的另一种写法。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
复制代码
上面代码中,constructor()、toString()、toValue()这三个方法,其实都是定义在Point.prototype上面。定义的内部方法是不可枚举的,而用ES5的方式去实现就是可枚举的。
class的实例
class的实例的行为大多数和ES5的保持一致。只是class不能直接像ES5构造函数那样直接使用,必须使用new关键字。
取值getter和setter
与ES5一致,可以使用get和set关键字。
属性表达式
类的属性可以使用表达式(变量)。
Class表达式
与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
static关键字
加上static关键字后,本方法(属性)不会被实例继承。只能直接通过类调用。且静态方法包含this的话,这个this是指向class本身的而不是指向实例。
父类的静态方法(属性)可以被子类继承。静态方法可以使用super关键字调用super对象。
私有方法和私有属性
利用Symbol实例来实现属性的私有,或者#提案来实现。
in运算符
判断私有属性时,in只能用在定义该私有属性的类的内部。
子类从父类继承的私有属性,也可以使用in运算符来判断。
7.class的继承
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。(ES5常用寄生组合继承)。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
复制代码
因此,可以使用这个方法判断,一个类是否继承了另一个类。猜一下这个方法其实就是招它的原型是否的构造函数是否等于父class。
super 关键字
super既可以当函数使用又可以当对象使用。
super()代表父类的构造函数(可以和es5构造器函数继承的实现一起理解,我猜想就是一个东西),super内部指向的是子类B的实例。
super作为对象式,指向父类的原型对象。在静态方法中,指向父类。
class的prototype和--proto--属性
1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性
原生构造函数的基础(基础引用数据类型的继承)
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
复制代码
上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}