重学JavaScript高级(六):以面向对象(ES5)搞懂原型原型链

面向对象原型继承(ES5)

普通对象原型(隐式原型)

JavaScript中的每个对象都有一个特殊的内置属性[[prototype]],它实际上是一个对象,指向的是另外一个对象

  • [[prototype]]的作用
    • 当我们通过引用对象的 key获取一个value时候 ,实际上会触发get的对象操作
    • 这个操作 首先会查找对象本身有没有这个key,如果有的话直接使用
    • 没有这个 key,那么会访问对象内置属性[[prototype]]指向的对象上,有没有这个属性
js 复制代码
let obj = {
  a: 100,
  b: 200,
};

//__proto__是浏览器自动加的
console.log(obj.__proto__);
//这是标准获取方法
console.log(Object.getPrototypeOf(obj));

函数的原型(显式原型)

所有的函数都有一个prototype属性 注意不是 __proto__

  • 我们知道,一个函数可以当作构造函数来使用,通过new,创建新的对象
js 复制代码
function Foo(){
    
}
/*
通过new方法创建的对象,内部会帮我们做以下事情
	1.首先在函数内部创建一个空对象
	2.将对象的__proto__(隐式原型)指向函数的prototype(显式原型的作用)
	3.将空对象赋值给this
	4.执行函数体中的代码
	5.将这个对象默认返回
*/
let newFoo = new Foo()
//通过步骤2我们可以得知,newFoo对象的隐式原型就是Foo函数的显式原型
newFoo.__proto__ === Foo.prototype
//无论通过Foo函数创建多少对象,这些对象的隐式原型都是相等的,均指向Foo函数的显式原型
  • 通过以上代码,将函数的显式原型赋值给对象的隐式原型 就是 显式原型的作用

  • 那么知道了它的作用,接下来可以看一个小案例,加深以下体会(将 方法放在原型上

    • 首先我们知道,我们可以通过构造函数来创建一个又一个的对象,这是为了将多个对象的共同的内容抽象到一起

      js 复制代码
      //创建一个学生构造函数
      function Student(stuName,age){
          this.stuName = stuName;
          this.age = age;
          //每个学生都有这个方法
          this.study = function () {
              console.log(this.stuName + "正在学习");
          };
      }
      
      //这样我们就可以创建多个对象
      let stu1 = new Student("zhangsan",18)
      let stu2 = new Student("zhangsan2",18)
      let stu3 = new Student("zhangsan3",18)
      let stu4 = new Student("zhangsan4",18)
    • 但是新的问题随之而来,每创建一个对象,都会生成一个study的方法,当生成足够多对象的时候,会占用内存

    • 那么通过 显式原型和隐式原型的关系 我们可以知道,将共有的方法提取到 显式原型上即可

      js 复制代码
      function Student(stuName, age) {
        this.stuName = stuName;
        this.age = age;
      }
      
      Student.prototype.study = function () {
          //这里的this指向,在前面的文章解释过,通过隐式绑定,指向的就是调用方法的对象
         console.log(this.stuName + "正在学习");
      };
      
      let stu1 = new Student("zhangsan", 20);
      let stu2 = new Student("lisi", 18);
      
      console.log(stu1.stuName, stu1.age);
      console.log(stu2.stuName, stu2.age);
      stu1.study()
      stu2.study()
    • 通过 将方法提取到构造函数的显式原型上,就可以解决以上的问题,以下是模拟的内存图

    • 通过内存模拟图我们可以看出

      • 无论创建多少对象,study方法都只会有一份
      • 每个对象都有 隐式原型 ,这个 隐式原型 是通过构造函数的 显式原型赋值过去的
      • 因此对象在调用study方法的时候:首先在自己身上找,发现没有就会顺着原型链找到所指向的显式原型
    • 因此当多个对象拥有共同的值时,我们可以放到构造函数的显式原型中

      • 由构造函数创建的所有对象,都会共享这些属性

显式原型中的属性

显式原型中有一个属性construtor

  • 我们打印显式原型,会发现有一个construtor的属性,同时打印这个属性发现是函数本身

    js 复制代码
    function Foo(){
        
    }
    
    console.log(Foo.prototype)//construtor
    console.log(Foo.prototype.construtor)//Foo
  • 试着画出以下代码的内存图

    js 复制代码
    function Student(stuName, age) {
      this.stuName = stuName;
      this.age = age;
    }
    Student.prototype.study = function () {
      console.log(this.stuName);
    };
    
    let stu1 = new Student("zhangsan", 20);
    let stu2 = new Student("lisi", 18);
    
    stu1.address = "河北";
    stu1.num = "18213";
    
    Student.prototype.classRoom = "ruanjian";
    
    stu1.classRoom = "jisuanji";
    console.log(stu2.classRoom);
    
    stu1.study();
    • 结合内存图,再看代码的运行情况,就可以明白以上的几点结论
      • 构造函数有自己的 显式原型对象
      • 构造函数创建对象时候,将对象的 隐式原型,指向自己的显式原型对象
      • 对象在调用方法,访问对象的时候,会优先在自己身上查找,没有的话再通过 隐式原型去查找

重写函数显式原型对象

  • 当我们要再函数显式原型上 添加大量的属性以及方法的时候,可以考虑重写显式原型

    js 复制代码
    function Student(stuName, age) {
      this.stuName = stuName;
      this.age = age;
    }
    
    Student.prototype.study = function () {
      console.log(this.stuName);
    };
    Student.prototype.num1 = 123;
    Student.prototype.num2 = 456;
    
    //可以这样重写显式原型对象
    Student.prototype = {
        study:function () {
            console.log(this.stuName);
        },
        num1:123,
        num2:456
    }
  • 但是重写完只会,我们会发现 重写的显式原型对象,缺失了constructor属性

    js 复制代码
    Student.prototype = {
        study:function () {
            console.log(this.stuName);
        },
        num1:123,
        num2:456,
        //可以直接这样写上该属性
        constructor:Student
    }
    //但是原本的constructor属性,是不可枚举的,且数据属性描述符需要特殊设置
    Object.defineProperty(Student.prototype,constructor,{
        value:Student
        //设置其他的数据属性描述符即可
    })

面向对象的特性 - 继承

面向对象的三大特性:封装、继承、多态

  • 封装:将多个对象中,相同的属性,写到一个类中的思想就是封装的思想
  • 继承:**继承是面向对象中非常重要的特性,**是多态的前提(纯面向对象中)
    • 可以帮助我们 **将重复的代码抽取到一个父类中,**子类只需要继承过来使用即可
    • 在JS中实现,需要了解继承
  • 多态:不同的对象再执行时表现出不同的形态(JS中不明显

对象的原型链

  • 默认形式的原型链

    js 复制代码
    //当我们创建一个对象的时候
    //这种创建方式相当于:let obj = new Object()
    //通过上面的学习,我们可以知道obj.__proto__ === Object.prototy
    //而Object.prototype作为对象也有自己的隐式原型,它的隐式原型指向null
    let obj = {
        name:"zhangcheng"
    }
  • 因此通过以上理论,我们可以对原型对象进行改造

    js 复制代码
    //当我们在obj上面查找message的时候,自身没有,就会顺着去查找它的原型,一层一层的往上找
    let obj = {
      name: "zhangcheng",
    };
    
    obj.__proto__ = {
      message: "hello aaa",
    };
    
    obj.__proto__.__proto__ = {
      message: "hello bbb",
    };
    obj.__proto__.__proto__.__proto__ = {
      message: "hello ccc",
    };
    
    console.log(obj.message);
  • 通过把对象的隐式原型改造 ,让其一层一层的去查找,这样就形成了原型链

  • 同时这就是继承的思想,我们可以创建一个父类,创建的子类去 继承父类子类实例出来的对象 ,在查找值的时候,首先在自身查找,之后会顺着原型链,去子类查找,子类若没有,就会查找父类,最后会查找到 Object的原型对象--->null

通过原型链实现继承

既然了解了理论,那么我们就开始使用原型链实现继承(ES5),有可能在使用定义变量用的ES6的语法,但是思想是最重要的

  • 现在有Peoson和Student两个类,要实现Student继承Person
js 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.running = function () {
  console.log("running");
};



function Student(name, age, classroom) {
  this.name = name;
  this.age = age;
  this.classroom = classroom;
}

Student.prototype.running = function () {
  console.log("running");
};
Student.prototype.study = function () {
  console.log("studying");
};
  • 在真正实现继承之前,我们先看以下操作
    • 这样虽然可以让stu1对象成功调用running
    • 但是在给Student类添加方法的时候,实际上是给Person类添加的方法(详见内存图)
js 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.running = function () {
  console.log("running");
};

//方式一:将Student的显式原型直接指向Peoson的显式原型
//或者将stu1的隐式原型指向Peoson的显式原型
Student.prototype = Person.prototype;

function Student(name, age, classroom) {
  this.name = name;
  this.age = age;
  this.classroom = classroom;
}

//Student.prototype.running = function () {
 // console.log("running");
//};
Student.prototype.study = function () {
  console.log("studying");
};
let stu1 = new Student("zhangcheng", 18, 2002);
stu1.running();
  • 很明显,以上的操作方法,并不是我们想要的

组合借用继承

  • 这种方式是ES5最常用的继承方法,但是只是基本实现了继承,依旧存在很多缺点
    • 通过第三方,实现父类方法的继承 (用父类创建一个对象,让子类的显式原型指向这个对象本身
    • 借用构造函数,实现父类属性的继承继承最大的用处是提高代码的复用,因此父类存在的属性,子类没有必要再写一份
js 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.running = function () {
  console.log("running");
};

//方式一:pass
// Student.prototype = Person.prototype;

//借用第三方,实现方法继承
let p = new Person();
Student.prototype = p;

function Student(name, age, classroom) {
  //借用构造函数的方式实现属性继承
  Person.call(this, name, age);
  this.classroom = classroom;
}

// Student.prototype.running = function () {
//   console.log("running");
// };
Student.prototype.study = function () {
  console.log("studying");
};
let stu1 = new Student("zhangcheng", 18, 2002);
console.log(stu1.name, stu1.age, stu1.classroom);
stu1.running();
stu1.study();
  • 通过组合借用继承的缺点
    • 无论在什么情况下 都会调用两次父类的构造函数
      • 通过父类创建一个实例,在子类的函数中,通过构造函数继承属性
    • 所有的子类实例 事实上会有两份父类属性
      • 一份存在实例对象上,另外一份存在父类创建的实例对象上

寄生组合式继承

是最终的解决方案

原型式继承函数

这种思想是道格拉斯·克罗克福德提出来的

  • 为了理解这种思想,我们需要回顾一下上面为了实现方法继承的目的

    • 在上面实现 方法继承,我们通过new 一个父类创建一个对象,但是会存在一些弊端
    • new 方法实现了以下的功能:
    • 1.创建一个空对象;
    • 2.将父类的 显式原型 赋值给 对象的 隐式原型
  • 因此为了 满足以上条件 ,且 不出现两份父类的元素,就出现了以下代码

    js 复制代码
    //传入的o是父类的显式原型对象
    function F(o){
        //让该函数的显式原型,指向父类的显式原型
        F.prototype = o
        //返回一个F类的对象,这个对象的隐式原型--->F类的显式原型--->o的显式原型
        return new F()
    }
    
    function Person(){
        
    }
    function Student(){
        
    }
    //这样就将Student的显式原型指向了新创建出来的对象
    Student.prototype = F(Person.prototype)
    
    //这样的继承方式,就不会有两份父类的属性
  • 通过以上思想,也可以使用Object.create()来创建

    Object.create()方法创建的对象,需要手动指定该对象的隐式原型指向哪里

  • 以下是实现的具体代码

    js 复制代码
    function inherit(Subtype, Supertype) {
      //将子类的显式原型对象,指向新创建的对象
      //该对象的隐式原型对象,指向的是父类的显式原型对象
      Subtype.prototype = Object.create(Supertype.prototype);
      //给子类的显式原型对象,设置constructor
      Object.defineProperty(Subtype.prototype, "construtor", {
        enumerable: false,
        configurable: true,
        writable: true,
        value: Subtype,
      });
    }
    //创建父类
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    //给父类添加方法
    Person.prototype.running = function () {
      console.log("running");
    };
    
    //创建子类
    function Student(name, age, classroom) {
      //借用构造函数的方式实现属性继承
      Person.call(this, name, age);
      this.classroom = classroom;
    }
    
    //使子类继承父类
    inherit(Student, Person);
    
    //给子类添加方法
    //一定要写在继承之后!!
    Student.prototype.study = function () {
      console.log("studying");
    };
    
    let stu1 = new Student("zhangcheng", 18, 2002);
    console.log(stu1.name, stu1.age, stu1.classroom);
    stu1.running();
    console.log(stu1);
    stu1.study();

Object是所有类的父类

  • 函数对象最终也是继承自Object

    js 复制代码
    Object.prototype.message = "zhangcheng"
    function foo(){}
    foo.message//zhangcheng
  • 所以,我们创建出来的对象,可以使用toString等方法,是因为Object上面有这些方法

原型继承关系图(重要)

  • 首先要明确:构造函数以函数角度 看时有相应的显式原型(prototype);以对象角度(每个函数都可看成一个对象 )有相应的 隐式原型__proto__
  • 第二点:所有对象的隐式原型 都指向**,创建它的,构造函数的显式原型**(Object.prototype的隐式原型除外,它指向null
    • 而构造函数的显式原型 ,本身就是对象,均由Object实例化出来的--->所以都指向Object的显式原型
  • 第三点:Function/Object/Foo/所有函数 都是Function的实例对象 (均new Function()方式创建)
    • 因此所有函数对象的隐式原型 均指向创建它的构造函数的显式原型
  • 因此 Object是Function的父类,Function是Object的构造函数

构造函数的实例方法和类方法

  • 实例方法:只能通过创建出来的实例进行调用
  • 类方法::只能通过构造函数调用
js 复制代码
//构造函数Person
function Person(name) {
  this.name = name;
}
//只能通过p1调用:实例方法
Person.prototype.study = function () {
  console.log("123");
};

//只能通过Person进行调用:类方法
Person.running = function () {
  console.log("456");
};

let p1 = new Person();
p1.study();
Person.running();

对象方法补充

hasOwnProperty

  • 判断某个属性,是否属于对象本身的(不是在原型上的属性)

    js 复制代码
    function Person(name){
        this.name = name
    }
    function Student(className){
        this.className = className
    }
    //Student 继承自 Person
    let stu1 = new Student(2002)
    stu1.hasOwnProperty("className")//true
    
    stu1.hasOwnProperty("name")//false

in/for in

  • 判断某个属性是否在某个对象上,或者原型上

    js 复制代码
    function Person(name){
        this.name = name
    }
    function Student(className){
        this.className = className
    }
    //Student 继承自 Person
    let stu1 = new Student(2002)
    console.log("name" in stu1)//true
    console.log("className" in stu1)//true
    
    //注意,for  in  遍历对象的时候,会将原型链上出现的属性都遍历出来
    for(let key in stu1){
        console.log(key)
    }

instanceof

  • 用于检测 构造函数的显式原型 ,是否出现在 某个实例对象原型链上

  • 也可以大致理解为,某个实例对象,是否是该构造函数创造出来的

  • 这个原理就是

    • 会顺着stu1的隐式原型去查找,看所对应的原型上的constructor是否返回了所写内容
    • 我们指定原型对象上面的constructor返回的是构造函数本身
    js 复制代码
    function Person(name){
        this.name = name
    }
    function Student(className){
        this.className = className
    }
    //Student 继承自 Person
    let stu1 = new Student(2002)
    
    console.log(stu1 instanceof Student)//true
    console.log(stu1 instanceof Person)//true

isPrototypeOf

  • 用于检测某个对象 ,是否出现在某个实例对象的原型链上
    • 也可以大致理解为:对象与对象之间的关系
    • 用的比较少
相关推荐
汪子熙1 分钟前
浏览器里出现 .angular/cache/19.2.6/abap_test/vite/deps 路径究竟说明了什么
前端·javascript·面试
Benzenene!3 分钟前
让Chrome信任自签名证书
前端·chrome
yangholmes88883 分钟前
如何在 web 应用中使用 GDAL (二)
前端·webassembly
jacy5 分钟前
图片大图预览就该这样做
前端
林太白7 分钟前
Nuxt3 功能篇
前端·javascript·后端
YuJie8 分钟前
webSocket Manager
前端·javascript
Mapmost24 分钟前
Mapmost SDK for UE5 内核升级,三维场景渲染效果飙升!
前端
Mapmost26 分钟前
重磅升级丨Mapmost全面兼容3DTiles 1.1,3DGS量测精度跃升至亚米级!
前端·vue.js·three.js
wycode33 分钟前
Promise(一)极简版demo
前端·javascript