重学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

  • 用于检测某个对象 ,是否出现在某个实例对象的原型链上
    • 也可以大致理解为:对象与对象之间的关系
    • 用的比较少
相关推荐
m0_748239334 分钟前
前端(Ajax)
前端·javascript·ajax
Fighting_p7 分钟前
【记录】列表自动滚动轮播功能实现
前端·javascript·vue.js
前端Hardy9 分钟前
HTML&CSS:超炫丝滑的卡片水波纹效果
前端·javascript·css·3d·html
技术思考者13 分钟前
HTML速查
前端·css·html
缺少动力的火车13 分钟前
Java前端基础—HTML
java·前端·html
Domain-zhuo26 分钟前
Git和SVN有什么区别?
前端·javascript·vue.js·git·svn·webpack·node.js
雪球不会消失了31 分钟前
SpringMVC中的拦截器
java·开发语言·前端
李云龙I42 分钟前
解锁高效布局:Tab组件最佳实践指南
前端
m0_748237051 小时前
Monorepo pnpm 模式管理多个 web 项目
大数据·前端·elasticsearch
JinSoooo1 小时前
pnpm monorepo 联调方案
前端·pnpm·monorepo