【机制】JavaScript的原型、原型链、继承

1.原型和原型链的概念

js在创建一个对象时,比如叫 obj,都会给他偷偷的加上一个引用,这个引用指向的是一个对象,比如叫 yuanxing
这个对象可以给引用它的对象提供属性共享,比如:yuanxing上有个属性name,可以被 obj.name访问到,
这个可以提供属性共享的对象,就称为前面对象的原型

而原型本身也是一个对象,所以它也会有一个自己的原型,这一层一层的延续下去,直到最后指向 null,这就形成的 原型链

那js的这一种是原型机制是怎么实现的呢?

2.原型的实现

我们先从一个例子来看:

//code-01
let obj = new Object({name:'xiaomin'})
console.log(obj.name)
console.log(obj.toString())

// xiaomin
// [object Object]

我们首先创建了一个对象obj,它有一个属性name
属性name是我们创建的,但是在创建obj的时候,并没有给它创建toString属性,为什么obj.toString()可以访问的到呢?

prototype 属性
我们先来看一下Object.prototype属性

我们发现这里有toString属性,所以其实Object.prototype就是obj的原型,按照原型的概念,它能够为obj提供属性共享
所以当obj.toString()的时候,先在obj的属性里找,没找到,就在obj的原型Object.prototype的属性上找,可以找到,所以调用成功

proto 属性
那么obj是怎么找到原型的呢?我们打印obj属性看一下:

我们发现obj除了我们创建的name以外,还有一个__proto__属性,它的值是一个对象,那么它等于什么呢?

我们发现obj.__proto__指向的就是Object.prototype

到此为止,我们可以简单总结一下js语言实现原型的基本机制了

  • 在创建一个对象obj时,会给它加上一个__proto__属性
  • __proto__属性 指向obj构造函数prototype对象
  • 当访问obj的某个属性时,会先在它自己的属性上找,如果没找到,就在它的原型(其实就是__proto__指向的对象)的属性上找

构造函数
这个有一个构造函数的概念,其实构造函数也就是普通函数,当这个函数被用来 new一个新对象时,它就被称为新对象的 构造函数
就上面的例子而言,Objec就是obj的构造函数,
这里要区别一下Objectobject的区别,前者是一个js内置的一个函数,后者是js的基本数据类型(number,string,function,object,undefined)

3.new 实际上做了什么

上面有说到new关键字,那么在它实际上做了什么呢?
上面代码code-01使用系统内置的函数Object来创建对象的,那么我们现在用自己创建的函数来创建一个新对象看看:

//code-02

function human(name){
  this.name = name
}
human.prototype.say = function(){
  alert('我叫'+this.name)
}
let xiaomin = new human('xiaoming')

console.log(xiaomin.name)
xiaomin.say()

这里的human就是新对象xiaoming的构造函数
我们把新创建的对象xiaoming打印出来看看:

我们看到xiaoming有一个属性name,并且xiaoming.__proto__完全等于构造函数的human.prototype,这就是它的原型
从这里我们可以总结一下new的基本功能:

  • 构造函数的this指向新创建的对象xiaoming
  • 为新对象创建原型,其实就是把新对象的__proto__指向构造函数的prototype

手写new
上面我们了解了new具体做了什么事情,那么它是怎么实现这些功能的呢?下面我们手写一个函数myNew来模拟一下new的效果:

//code-03
    function human(name, age) {
      this.name = name;
      this.age = age;
    }
    human.prototype.say = function () {
      console.log("my name is " + this.name);
    };

    xiaoming = myNew(human, "xiaoming", 27);

    function myNew() {
      let obj = new Object();
      //取出函数的第一个参数,其实就是 human函数
      let argArr = Array.from(arguments);
      const constructor = argArr.shift();
      // 指定原型
      obj.__proto__ = constructor.prototype;
      //改变函数执行环境
      constructor.apply(obj, argArr);
      return obj;
    }

    xiaoming.say();

我们先把新对象xiaoming打印出来看一下:

我们发现这和上面的代码code-02的效果是一样的
上面代码code-03里面如果对apply的作用不太熟悉的,可以另外了解一下,其实也很简单,意思就是:在obj的环境下,执行constructor函数,argArr是函数执行时的参数,也就是指定了函数的this

4.继承的实现

其实就上面的内容,就可以对js的原型机制有个基本的了解,但是一般面试的时候,如果有问到原型,接下来就会问 能不能实现 继承的功能,所以我们来手写一下 原型的继承,其实所用到的知识点都是上面有提到的

继承的概念
我们先来说下继承的概念:
继承其实就是 一个构造函数(子类)可以使用另一个构造函数(父类)的属性和方法,这里有几点注意的:

  • 继承是 构造函数 对 另一个构造函数而言
  • 需要实现属性的继承,即 this的转换
  • 需要实现方法的继承,一般就是指 原型链的构建

继承的实现
基于上面的3点要素,我们先直接来看代码:

// code-04
   // 父级 函数
    function human(name) {
      this.name = name;
    }
    human.prototype.sayName = function () {
      console.log("我的名字是:", this.name);
    };

    // 子级 函数
    function user(args) {
      this.age = args.age;
      //1.私有属性的继承
      human.call(this, args.name); 
      //2.原型的继承
      Object.setPrototypeOf(user.prototype, human.prototype); //原型继承-方法1
      // user.prototype.__proto__ = human.prototype; // 原型继承-方法2
    }
    // 因为重新赋值了prototype,所以放置 user 外部
    // user.prototype = new human();//原型继承-方法3
    // user.prototype = Object.create(human.prototype);//原型继承-方法4

    user.prototype.sayAge = function () {
      console.log("我的年龄是:", this.age);
    };
    let person = new human("人类");
    let xiaoming = new user({ name: "xiaoming", age: 27 });

    console.log("----父类-----");
    console.log(person);
    person.sayName();

    console.log("----子类-----");
    console.log(xiaoming);
    xiaoming.sayName();
    xiaoming.sayAge();

我们先来看下打印的结果:

从打印结果,我们可以看到xiaoming拥有person的属性和方法(name,sayName),又有自己私有的属性方法(age,sayAge),这是因为构造函数user实现了对human的继承。
其实实现的方法无非也就是我们前面有说到的 作用域的改变和原型链的构造,其中作用域的改变(this指向的改变)主要是两个方法:call和apply,原型链的构造原理只有一个,就是对象的原型等于其构造函数的prototype属性,但是实现方法有多种,代码code-04中有列出4种。
从上面的例子来看原型链的指向是:xiaoming->user.prototype->human.prototype

5.class和extends

我们可能有看到一些代码直接用 classextends关键字来实现类和继承,其实这是ES6的语法,其实是一种语法糖,本质上的原理也是相同的。我们先来看看基本用法:
用法

//code-05
   class human {
      //1.必须要有构造函数
      constructor(name) {
        this.name = name;
      }//2.不能有逗号`,`
      sayName() {
        console.log("sayName:", this.name);
      }
    }

    class user extends human {
      constructor(params) {
        //3.子类必须用`super`,调用父类的构造函数
        super(params.name);
        this.age = params.age;
      }
      sayAge() {
        console.log("sayAge:", this.age);
      }
    }

    let person = new human("人类");
    let xiaoming = new user({ name: "xiaoming", age: 27 });

    console.log("----<human> person-----");
    console.log(person);
    person.sayName();

    console.log("----<user> xiaoming-----");
    console.log(xiaoming);
    xiaoming.sayName();
    xiaoming.sayAge();

执行结果:

我们看到执行的结果和上面的代码code-04是一样的,但是代码明显清晰了很多。几个注意的地方:

  • class类中必须要有构造函数constructor,
  • class类中的函数不能用 ,分开
  • 如果要继承父类的话,在子类的构造函数中,必须先执行 super来调用的父类的构造函数

相同
上面有说class的写法其实原理上和上面是一样的,我们来验证一下

  1. 首先看看userhuman是什么类型

    这里看出来了,所以虽然被class修饰,本质上还是函数,和代码code-04中的user,human函数是一样的

  2. 再来看看prototype属性

    这里看出来sayName,sayAge都是定义在human.prototypeuser.prototype上,和代码code-04中也是一样的

  3. 我们再来看看原型链

    这与代码code-04中的原型链的指向也是一样:xiaoming->user.prototype->human.prototype

差异
看完相同点,现在我们来看看不同点:

  1. 首先写法上的不同
  2. class声明的函数,必须要用new调用
  3. class内部的成员函数没有prototype属性,不可以用new调用
  4. class 内的代码自动是严格模式
  5. class声明不存在变量提升,这一点和 let一样,比如:
    //code-06
    console.log(name_var);
    var name_var = "xiaoming";
    //undefined,不会报错,var声明存在变量提升
    
    console.log(name_let);
    let name_let = "xiaoming";
    // Uncaught ReferenceError: Cannot access 'name_let' before initialization
    //报错,let声明不存在变量提升
    
    new user();
    class user {}
    // Uncaught ReferenceError: Cannot access 'user' before initialization
    //报错,class声明不存在变量提升
    
    
  6. class内的方法都是不可枚举的,比如:
      //code-07
      class human_class {
      constructor(name) {
        this.name = name;
      }
      sayName() {
        console.log("sayName:", this.name);
      }
    }
    function human_fun(name) {
      this.name = name;
    }
    human_fun.prototype.sayName = function () {
      console.log("sayName:", this.name);
    };
    console.log("----------human_class-----------");
    console.log("prototype属性", human_class.prototype);
    console.log("prototype 枚举", Object.keys(human_class.prototype));
    
    console.log("----------human_fun-----------");
    console.log("prototype属性", human_fun.prototype);
    console.log("prototype 枚举", Object.keys(human_fun.prototype));
    
    运行结果:

6.总结

简单总结一下:

  • 每个对象在创建的时候,会被赋予一个__proto__属性,它指向创建这个对象的构造函数的prototype,而prototype本身也是对象,所以也有自己的__proto__,这就形成了原型链,最终的指向是 Object.prototype.__proto__ == null
  • 可以通过new,Object.create(),Object.setPrototypeOf(),直接赋值__proto__等方法为一个对象指定原型
  • new操作符实际做的工作是:创建一个对象,把这个对象作为构造函数的this环境,并把这个对象的原型(proto)指向构造函数的prototype,最后返回这个对象
  • 继承主要实现的功能是:this指向的绑定,原型链的构建
  • ES6的语法classextends可以提供更为清晰简洁的写法,但是本质上的原理大致相同
posted @ 2021-01-05 14:23  木子草明  阅读(451)  评论(1编辑  收藏  举报