06-原型和原型链

文章目录

  • [1. 认识原型](#1. 认识原型)
    • [1.1 ES5 -- **对象**的原型](#1.1 ES5 -- 对象的原型)
    • [1.2 ES5 -- **构造函数**的原型](#1.2 ES5 -- 构造函数的原型)
    • [1.3 ES5 -- 重写原型对象(不推荐)](#1.3 ES5 -- 重写原型对象(不推荐))
  • [2. 原型链](#2. 原型链)
    • [2.1 ES5 -- 默认对象的原型链](#2.1 ES5 -- 默认对象的原型链)
    • [2.2 ES5 -- 利用**原型链**实现继承](#2.2 ES5 -- 利用原型链实现继承)
    • [2.3 ES5 -- 借用**构造函数**实现属性继承](#2.3 ES5 -- 借用构造函数实现属性继承)
    • [2.4 ES5 -- 原型式继承函数](#2.4 ES5 -- 原型式继承函数)
    • [2.5 ES5 -- 对象补充方法](#2.5 ES5 -- 对象补充方法)
  • [3. 原型关系](#3. 原型关系)
    • [3.1 原型继承关系图](#3.1 原型继承关系图)
    • [3.2 ES6的`class`](#3.2 ES6的class)
      • [3.2.1 class定义类](#3.2.1 class定义类)
      • [3.2.2 访问器](#3.2.2 访问器)
    • [3.3 extends实现继承](#3.3 extends实现继承)

认识一下原型和原型链。

1. 认识原型

原型是什么?怎样获取?

1.1 ES5 -- 对象的原型

每个对象身上都有一个[[prototype]]内置属性,这个特殊的对象可以指向另外一个对象。

  • 当我们通过引用对象的key来获取value时,就会触发[[Get]]操作;
  • 这个操作首先会在对象本身进行寻找,如果有的话就使用它;
  • 没有的话就在对象的[[prototype]]属性指向的对象中寻找,如果还没有的话就继续找对象[[prototype]]属性指向的对象的[[prototype]]属性指向的对象中寻找,直到找到或者找不到为止。

获取对象原型的方法

  1. __proto__属性 obj.proto

    这种方式存在浏览器的兼容性问题,所以不建议使用;

  2. getPrototypeOf() Object.getPrototypeOf(obj)

    同样的,设置原型可使用setPrototypeOf(对象,对象要指向的原型)方法;

javascript 复制代码
    const obj = {
    name: 'prototype'
}

console.log(obj);

console.log(obj.__proto__);
console.log(Object.getPrototypeOf(obj));
console.log(obj.__proto__ === Object.prototype); // true

1.2 ES5 -- 构造函数的原型

函数本身也是对象,它身上有一个[[prototype]]属性,这个属性是一个对象,这个对象就是函数的实例对象。

区别于对象的获取方式,函数的[[prototype]]属性是显式原型 ,而函数对象的__proto__隐式原型

通过new关键字创建对象:

  1. 创建空对象;
  2. 将空对象赋值给this;
  3. 将对象的显式原型赋值给隐式原型;

案例:

javascript 复制代码
function Student(name, age) {
  this.name = name;
  this.age = age;

  this.studying = function () {
    console.log(this.name + ' is studying');
  }
}

const s1 = new Student('xuexi', 18);
const s2 = new Student('student', 18);

console.log(s1.studying === s2.studying); // false
  • 若这样定义函数,那么每次通过new创建对象时都会创建一个新的studying方法;
  • 但若是将该方法定义在Student.prototype对象中,那么studying方法只会创建一次,所有对象实例都会共享这个方法。
javascript 复制代码
Student.prototype.studying = function () {
  console.log(this.name + ' is studying');
}
const s1 = new Student('xuexi', 18);
const s2 = new Student('student', 18);

console.log(s1.studying === s2.studying); // true

函数的显式原型对象[[prototype]]上有一个[[constructor]]属性,和这个属性指向函数本身。

javascript 复制代码
console.log(Student.prototype.constructor === Student); // true

案例:

javascript 复制代码
function Person(name, age) {
  this.name = name
  this.age = age
}

const p1 = new Person('p1', 18)
const p2 = new Person('p2', 20)

Person.prototype.running = function () {
  console.log(this.name + ' is running')
}

查找某个属性时,首先在对象本身中寻找,如果没有再在原型对象上寻找,如果没有再在原型对象的原型对象上(Object.prototype)寻找,直到找到或者找不到为止。

1.3 ES5 -- 重写原型对象(不推荐)

如果要在原型对象上添加很多属性,那么我们一般会重写原型对象。

我们可以通过上面的方法重写原型对象,但注意不要忘记添加[[constructor]]属性。

在重写前我们发现原型对象的[[constructor]]的属性描述符是不可枚举的:

javascript 复制代码
console.log(Object.keys(Person.prototype))  // ['running']

但是按照我们重写的方式来看,[[constructor]]属性也将会被枚举,所以我们需要通过属性描述符对这个属性进行更改。

javascript 复制代码
Object.defineProperty(Person.prototype, "constructor", {
  configurable: true,
  value: "Person",
  enumerable: false,
  writable: true
})

2. 原型链

2.1 ES5 -- 默认对象的原型链

当在对象身上查找某个属性时,会现在真神寻找,自身没有会在原型__proto__上寻找,如果没有的话会在原型的原型__proto__上寻找,最终查找到Object.prototype,它指向null,直到找到或者找不到为止。

javascript 复制代码
const obj = {};
// 相当于  const obj = new Object(); 构造函数的原型
// 即 obj.__proto__ = Object.prototype;

2.2 ES5 -- 利用原型链实现继承

现定义一个Person类,定义一个Student类,实现Student继承Person

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.say = function () {
    console.log('hello world');
}
function Student(name, age) {
    this.name = name;
    this.age = age;
}
Student.prototype.say = function () {
    console.log('hello world');
}

Student.prototype.studying = function () {
    console.log(this.name + ' is studying');
}
  1. 方式一 -- Student.prototype = Person.prototype;

    这种方式会产生问题:

    本来只有Student类才有的studying方法,但是通过这种方式,Student类和Person类都拥有studying方法。也就没有实现继承。

  2. 方式二 -- Student.prototype = new Person();Student.prototype = p;

    这种方式将Student类的studying方法放在p对象身上,而Person原型上没有,实现了继承。

2.3 ES5 -- 借用构造函数实现属性继承

javascript 复制代码
// 在利用原型链实现继承的基础上
function Student(name, age) {
    Person.apply(this,name,age)
    this.name = name;
    this.age = age;
}

const student = new Student("chender", 22)

组合继承的弊端

  1. 创建对象时,会调用两次父类的构造函数,一次是Student.prototype = new Person();,一次是Student.prototype = p;
  2. 所有的子类实例都有两份父类的属性,一份在自身,一份在原型上;

2.4 ES5 -- 原型式继承函数

从上面的例子中可以发现,想要实现继承就需要将父类和子类连接起来:

  1. 创建新对象;
  2. 将新对象的原型指向父类原型;
  3. 子类原型指向所创建的对象;
javascript 复制代码
function createObj(o){
//     无兼容性创建对象方式
   function F(){}
   F.prototype = o
   return new F()
   
//    有兼容性创建方式:
//    return Object.craete(SuperType.prototype)
}

function inderit(SubType, SuperType){
    SubType.prototype = craeteObj(SuperType.prototype)
    Object.defineProperty(SubType.prototype, "constructor", {
       enumerable:false,
       configurable: true,
       writable:true,
       value: SubType
   })
}

继承的最终写法(寄生组合式继承):

javascript 复制代码
function Person(name, age) {
   this.name = name;
   this.age = age;
}

Person.prototype.eating = function () {
   console.log('eating')
}

function Student(name, age, sex, grade) {
   Person.apply(this, [name, age])
   this.sex = sex;
   this.grade = grade;
}

function createObj(o) {
   function F() { }
   F.prototype = o
   return new F()

   //    return Object.create(o)
}

function inherit(SubType, SuperType) {
   SubType.prototype = createObj(SuperType.prototype)
   Object.defineProperty(SubType.prototype, "constructor", {
      enumerable: false,
      writable: true,
      configurable: true,
      value: SubType
   })
}

补充 -- 寄生式继承函数

  • 寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想,并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推
    广的;
  • 寄生式继承的思路是结合原型类继承和工厂模式的一种方式;
  • 即创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回;

从图中可以看出,Student继承自Person,同样的,Person也继承自Object,可得**Object是所有类的子类**。

2.5 ES5 -- 对象补充方法

  1. hasOwnProperty() -- 判断某个属性是否存在于对象自身,而不是原型的

    obj.hasOwnProperty("name")

  2. in操作符 -- 判断某个属性是否存在于对象身上(包括原型)

    "name" in obj

    for...in 可遍历对象的所有可枚举属性,包括原型上的属性

  3. instanceof -- 用于检测构造函数的prototype

javascript 复制代码
   function Person(name, age) {}
   function Student(name, age) {}
   inherit(Student, Person)
   const p = new Student("chender", 22)
   console.log(p instanceof Student) // true
   console.log(p instanceof Person)  // true
   console.log(p instanceof Object)  // true
  1. isPrototypeof() -- 用于判断某个对象是否出现在某个实例对象的原型链上 (了解即可) Student.prototype.isPrototypeof§ // p是否在Student.prototype的原型链上

3. 原型关系

3.1 原型继承关系图


图片来源:javascript_layout_tree

注:

  1. 所有函数(比如Person, Object)都是Function的实例对象
  2. Function也是Function的实例对象;
  3. 原型对象创建时默认其隐式原型指向Object的显式原型;
  4. 推导得:Object是Person, Function的父类

补充 -- 构造函数的类方法和实例方法:

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 实例方法
Person.prototype.eating = function () {
   console.log("eating")
}

// 类方法
Person.randomPerson = function (){
    return new Person("abc", 18)
}

const p1 = new Person("name", 18)

有了上面这段代码,现在来思考一个问题:

  1. Person.eating() 会执行吗?
    答案是 不会。

    • 因为Person是Function的实例对象,是Object,且Person原型上没有eating方法,接着在Object原型上找也没有找到,所以会报错。
    • 这种定义在构造函数原型上的方法就叫做实例方法(只有创建实例对象才能调用)。
  2. const p = Person.randomPerson() 会执行吗?
    答案是 会。

    • 因为randomPerson方法在Person对象身上,直接调用即可获得;
    • 这种定义在构造函数对象上的方法就叫做类方法。

3.2 ES6的class

3.2.1 class定义类

ES6新的标准中使用class关键字定义类。

javascript 复制代码
class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    
    // 实例方法
    // 本质是Person.prototype.eating = function(){}
    eating(){
       console.log("eating")
    }
    
//     类方法
    static sleeping(){
       console.log("sleeping")
    }
}

class方式定义类,创建对象和构造函数方式定义类创建对象本质上是一样,但class定义的类不能作为普通函数进行调用:

3.2.2 访问器

如何更灵活的对对象或者类的属性进行管理?我们可以使用访问器来实现。

  1. 对象访问器
javascript 复制代码
   const obj = {
      "_name": "chen",
      get name(){
           return this._name
      },
      set name(value){
           this._name = value
      }
   }
   
   // 也可以使用Object.defineProperty()进行访问器定义
   Object.defineProperty(obj, "name", {
      configurable: true,
      enumerable: true,
      get(){},
      set(value){}
   })
  1. 类访问器
javascript 复制代码
class Person{
   constructor(name){
       this.name = name
   }
   get name(){
        return this.name
   }
   set name(value){
        this.name = value
    }
}

3.3 extends实现继承

javascript 复制代码
class Person{
    constructor(name){
        this.name = name
    }
    say(){}
}

class Student extends Person{
    constructor(name, age){
        super(name)
        this.age = age
    }
}

class中的super关键字:

  1. super.method(...) 来调用一个父类方法;
  2. super(...) 来调用父类的constructor(...)

注:

  1. 在子类中使用this或者返回默认对象前必须先通过super调用父类构造函数;
  2. super的使用位置:构造函数、实例方法、类方法;
  3. super只能调用同名函数,不能调用不同名函数;
javascript 复制代码
   class Student extends Person {
       // ...
       static sleep() {
           super.sleep()
           // super.eat()   // 报错
           console.log('sleeping')
       }
   }
相关推荐
curdcv_po几秒前
🔥🔥🔥结合 vue 或 react,去写three.js
前端·react.js·three.js
猫头_29 分钟前
uni-app 转微信小程序 · 避坑与实战全记录
前端·微信小程序·uni-app
天生我材必有用_吴用31 分钟前
网页接入弹窗客服功能的完整实现(Vue3 + WebSocket预备方案)
前端
海拥37 分钟前
8 Ball Pool:在浏览器里打一局酣畅淋漓的桌球!
前端
Cache技术分享44 分钟前
148. Java Lambda 表达式 - 捕获局部变量
前端·后端
明长歌1 小时前
【javascript】Reflect学习笔记
javascript·笔记·学习
YGY Webgis糕手之路1 小时前
Cesium 快速入门(二)底图更换
前端·经验分享·笔记·vue
神仙别闹1 小时前
基于JSP+MySQL 实现(Web)毕业设计题目收集系统
java·前端·mysql
前端李二牛1 小时前
Web字体使用最佳实践
前端·http
YGY_Webgis糕手之路1 小时前
Cesium 快速入门(六)实体类型介绍
前端·gis·cesium