javascript基础知识问答——原型和原型链

  • 1.理解原型设计模式以及JavaScript中的原型规则
  • 2.instanceof的底层实现原理,手动实现一个instanceof
  • 3.实现继承的几种方式以及他们的优缺点
  • 4.至少说出一种开源项目(如Node)中应用原型继承的案例
  • 5.可以描述new一个对象的详细过程,手动实现一个new操作符
  • 6.理解es6 class构造以及继承的底层实现原理

一. 理解原型设计模式以及javascript中的原则规则

原型设计模式,这种设计模式就是创建一个共享的原型,并通过拷贝这些原型创建新的对象。用于创建重复的对象,这种类型的设计模式属于创建型模式,它提供了一种创建对象的不错选择。可以通过原型链实现原型设计模式。
原型设计模式主要的特性

  • 所有函数(类)以及部分数据类型(number数值型、string字符串型、array数组型、function函数型)具有prototype属性;
  • 在prototype属性上设置的属性,所有的实例均可以共享;
  • 在实例上可修改prototype属性上设置的属性
    • 值类型修改:仅限当前实例发生变更
    • 引用类型修改:
      • 直接修改引用类型,只影响当前实例的值,并且在修改后,引用地址发生变化,后续对该实例上所有属性更改只对当前实例起作用
      • 修改应用类型的属性或者项,父类就会发生更改,故会影响到所有实例的值
  • 类可以直接设置静态属性,可以只用通过 ' 类名.属性名 = 值 ' 来设置和访问,但实例不可访问;
var person = {
    name: 'zhangsan',
    age: 25,
    sayHello: function(){
        return this.name
    }
}//先构建一个类

var man = Object.create(person,{
    job: {
        value: 'IT'
    }
});//利用Object.create(prototype, optionalDescriptorObjects)来使用现有的对象来提供新创建的对象的__proto__
console.log(man.sayHello())  // zhangsan
console.log(man.age) // 25
console.log(man.job)  // IT
console.log(man.__proto__ === prototype)  //true

可以看到,我们通过Object.create()创建对象,此时新建的对象就继承自构造器的原型对象,及继承了初始的person,而且可以查看返回值的proto属性和person内的prototype是一样的。我们常说一个对象的原型,实际上我们是在说这个对象的构造器是有原型的。我们通过该方式,创建了一个新的对象,并且继承了自构造器的属性,这就是原型设计模式。
在JavaScript中,对象可以使用原型克隆来实现获取以及继承原型对象的属性和方法,很多情况下开发者会使用原型对象的Object.prototype,但是今天我们介绍了也可以通过Object.create()方法实现对我们需要的目标对象为原型的克隆操作,同时也可以通过修改构造器的prototype指向来复制其它对象的属性及方法

原型中的一些规则:

  1. 所有的引用数据类型(array数组类型,object对象类型,function函数类型)都具有自由扩展的属性;
  2. 所有的引用数据类型都有一个proto属性即隐式原型,其属性值是一个普通对象;
  3. 所有的函数,都具有一个prototype即显式原型,其属性值也是一个普通对象;
  4. 所有的引用数据类型,它的隐式原型(proto)都是指向其构造函数的显示原型(prototype),即(obj.proto === Object.prototype);
  5. 如果想获取或利用某个对象的属性或方法时,这个对象本身没有这个属性或防范,那么我们可以去它的proto即它指向的构造函数的prototype上去查找;

二. instanceof的底层实现原理,手动实现一个instanceof

查看某对象的prototype属性指向的原型对象是否在另一对象的原型链上,如果在就返回true,如果不在返回false

123 instanceof Number, //false
'dsfsf' instanceof String, //false
false instanceof Boolean, //false
[1, 2, 3] instanceof Array, //true
{a: 1} instanceof Object, //true
function () {} instanceof Function, //true
undefined instanceof Object, //false
null instanceof Object, //false
new Date() instanceof Date, //true
/^[a-zA-Z]{5,20}$/ instanceof RegExp, //true
new Error() instanceof Error //true

三. 实现继承的几种方式以及他们的优缺点

首先,得有一个父类

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};
  1. 原型链的继承
    核心: 将父类的实例作为子类的原型
function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到
  3. 简单,易于实现
    缺点:
  • 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  • 无法实现多继承
  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参
  1. 构造函数
    核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:

  • 解决了1中,子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点:

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
  1. 实例继承
    核心:为父类实例添加新特性,作为子类实例返回
function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false

特点:

  • 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  • 实例是父类的实例,不是子类的实例
  • 不支持多继承
  1. 拷贝继承
function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  Cat.prototype.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true

特点:

  • 支持多继承

缺点:

  • 效率较低,内存占用高(因为要拷贝父类的属性)
  • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
  1. 组合继承
    核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();// 组合继承也需要修复构造函数指向
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true

特点:
* 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例
* 不存在引用属性共享问题
* 可传参
* 函数可复用

缺点:
* 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

  1. 寄生组合继承
    核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true感谢 @bluedrink 提醒,该实现没有修复constructor。Cat.prototype.constructor = Cat; // 需要修复下构造函数

特点:堪称完美
缺点:实现较为复杂

四. 至少说出一种开源项目(如Node)中应用原型继承的案例

五. 可以描述new一个对象的详细过程,手动实现一个new操作符

1、创建一个新的对象
2、把obj的proto指向fn的prototype,实现继承
3、改变this的指向,执行构造函数、传递参数,fn.apply(obj,) 或者 fn.call()
4、返回新的对象obj

  function Dog(name) {
        this.name = name
        this.say = function () {
            console.log('name = ' + this.name)
        }
    }
    function Cat(name) {
        this.name = name
        this.say = function () {
            console.log('name = ' + this.name)
        }
    }
    function _new(fn, ...arg) {
        const obj = {}; //创建一个新的对象
        obj.__proto__ = fn.prototype; //把obj的__proto__指向fn的prototype,实现继承
        fn.apply(obj, arg) //改变this的指向
        return Object.prototype.toString.call(obj) == '[object Object]'? obj : {} //返回新的对象obj
    }
 
    //测试1
    var dog = _new(Dog,'aaa')
    dog.say() //'name = aaa'
    console.log(dog instanceof Dog) //true
    console.log(dog instanceof Cat) //true
    //测试2
    var cat = _new(Cat, 'bbb'); 
    cat.say() //'name = bbb'

六. 理解es6 class构造以及继承的底层实现原理

javascript使用的是原型式继承,我们可以通过原型的特性实现类的继承,
es6为我们提供了像面向对象继承一样的语法糖。

class Parent {
  constructor(a){
    this.filed1 = a;
  }
  filed2 = 2;
  func1 = function(){}
}

class Child extends Parent {
    constructor(a,b) {
      super(a);
      this.filed3 = b;
    }
  
  filed4 = 1;
  func2 = function(){}
}

下面我们借助babel来探究es6类和继承的实现原理。

1. 类的实现

转换前

class Parent {
  constructor(a){
    this.filed1 = a;
  }
  filed2 = 2;
  func1 = function(){}
}

转换后:

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Parent = function Parent(a) {
  _classCallCheck(this, Parent);
  this.filed2 = 2;
  this.func1 = function () { };
  this.filed1 = a;
};

可见class的底层依然是构造函数:
1.调用_classCallCheck方法判断当前函数调用前是否有new关键字。

构造函数执行前有new关键字,会在构造函数内部创建一个空对象,将构造函数的proptype指向这个空对象的proto,并将this指向这个空对象。如上,_classCallCheck中:this instanceof Parent 返回true。

若构造函数前面没有new则构造函数的proptype不会不出现在this的原型链上,返回false。

2.将class内部的变量和函数赋给this。
3.执行constuctor内部的逻辑。
4.return this (构造函数默认在最后我们做了)。

2. 继承实现

转换前:

class Child extends Parent {
    constructor(a,b) {
      super(a);
      this.filed3 = b;
    }
  
  filed4 = 1;
  func2 = function(){}
}

转换后:
我们先看Child内部的实现,再看内部调用的函数是怎么实现的:

var Child = function (_Parent) {
  _inherits(Child, _Parent);
  function Child(a, b) {
    _classCallCheck(this, Child);
    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a));
    _this.filed4 = 1;
    _this.func2 = function () {};
    _this.filed3 = b;
    return _this;
  }
  return Child;
}(Parent);
1.调用_inherits函数继承父类的proptype。

_inherits内部实现:

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, enumerable: false, writable: true, configurable: true }
  });
  if (superClass)
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

(1) 校验父构造函数。
(2) 典型的寄生继承:用父类构造函数的proptype创建一个空对象,并将这个对象指向子类构造函数的proptype。
(3) 将父构造函数指向子构造函数的proto(这步是做什么的不太明确,感觉没什么意义。)

2.用一个闭包保存父类引用,在闭包内部做子类构造逻辑。
3.new检查。
4.用当前this调用父类构造函数
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a));

这里的Child.proto || Object.getPrototypeOf(Child)实际上是父构造函数(_inherits最后的操作),然后通过call将其调用方改为当前this,并传递参数。(这里感觉可以直接用参数传过来的Parent)

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

校验this是否被初始化,super是否调用,并返回父类已经赋值完的this。

5.将行子类class内部的变量和函数赋给this。
6.执行子类constuctor内部的逻辑。

可见,es6实际上是为我们提供了一个“组合寄生继承”的简单写法。

3. super

super代表父类构造函数。
super.fun1() 等同于 Parent.fun1() 或 Parent.prototype.fun1()。
super() 等同于Parent.prototype.construtor()

当我们没有写子类构造函数时:

var Child = function (_Parent) {
  _inherits(Child, _Parent);

  function Child() {
    _classCallCheck(this, Child);

    return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
  }
  return Child;
}(Parent);

可见默认的构造函数中会主动调用父类构造函数,并默认把当前constructor传递的参数传给了父类。
所以当我们声明了constructor后必须主动调用super(),否则无法调用父构造函数,无法完成继承。
典型的例子就是Reatc的Component中,我们声明constructor后必须调用super(props),因为父类要在构造函数中对props做一些初始化操作。

参考链接:https://blog.csdn.net/Kreme/java/article/details/102940455
https://blog.csdn.net/Kreme/java/article/details/102975973
https://www.cnblogs.com/humin/p/4556820.html
https://blog.csdn.net/qq_39985511/java/article/details/87692673
https://blog.csdn.net/qq_34149805/java/article/details/86105123

你可能感兴趣的:(javascript基础知识问答——原型和原型链)