深入对象的原型和继承

一.创建对象的方式

1.通过字面量的方式创建

js 复制代码
  //创建的对象的方式
  var obj = { name: "mm", age:18}
  console.log(obj); // {name: 'mm', age: 18}

2.工厂模式创建对象

js 复制代码
  //工厂方式创建
  function createObj(name, age){
    var obj = {}
    obj.name = name
    obj.age = age

    return obj
  }

  const obj2 = createObj("kk", 19)
  console.log(obj2);

3.通过构造函数创建

在MDN官网上有单独对new关键字解释和描述地址
其中在对new的描述中有主要的几点

  1. 在内存中创建一个空对象
  2. 这个对象的内部的【prototype】属性会被赋值为该构造函数的prototype属性
  3. 构造函数内部的this,会指向创建出来的空对象
  4. 执行函数的内代码
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象 this
js 复制代码
function Person(name,age,height){
    this.name = name;
    this.age = age;
    this.height = height;
    this.eating = function(){
        console.log(this.name, '在吃东西');
    }
}

var p2 = new Person('lee',20, 1.80);
console.log(p2);

二.原型

从构造函数创建对象中, 我们发现第2点的描述中涉及到原型了,其实每个对象都有原型

1.普通对象的原型

每个普通的对象都会有一个原型属性[[prototype]]

js 复制代码
var obj = { name: 'mm'};
console.log(obj); 
//打印的结果可以看到 [[Prototype]]属性,这个属性是一个对象
1.1 如何查看这个原型

通过下面的方式查看原型:

1.对象的__proto__ 属性获取(注意:这个属性是浏览器自己实现, 不一定所有的浏览器都有这个属性)

2.通过Object.getPrototypeOf(obj)方法获取

3.下面打印的原型, 出来的结果是一个对象, 我们称这个对象是原型对象

js 复制代码
var obj = { name: 'mm'};

//浏览器中使用 __proto__ 查看原型
console.log(obj.__proto__); 

//代码查看原型 object 类型
var proto =  Object.getPrototypeOf(obj) ;
console.log("proto----",proto);
1.2原型的作用

1.我们知道创建的对象是 key-value形式, 我们可以通过key获取到对象的value
2.当我们用一个key, 去对象obj中获取value时,如果在obj中获取不到, 就会去obj的原型中查找,如果找不到就会返回undefined

js 复制代码
// 当我们从一个对象中获取某一个属性的值时
// 1. 在当前对象中去查找对应的属性, 如果找到就直接使用
// 2. 如果没有找到, 那么会沿着它的原型去查找 [[prototype]]
console.log(obj.age); //undefined
//给原型对象上添加age
obj.__proto__.age = 18;
console.log(obj.age); //18

2.函数的原型

每一个函数都有一个prototype原型属性, 因为函数也是一个对象,所以还有一个__proto__

  1. 一般我们把prototype称为函数的显式原型
  2. __proto__我们称为函数作为对象的隐式原型
js 复制代码
// 1.将函数看成是普通的对象的时候,他具备一个__proto__隐式原型
function foo(){}
//把函数是一个对象
console.log("对象原型__proto__:",foo.__proto__);

// 函数它因为是一个函数, 函数也会有一个prototype显示原型
// 2.每个函数默认有一个显示的原型prorotype
console.log("函数原型prototype:",foo.prototype);

从上面的打印中, 函数作为对象 和 函数是函数都有原型, 那么对象原型__proto__原型 和 函数prototype原型 有什么关系?

2.1 函数对象原型__proto__ 和 函数原型(prorotype)的关系

想要搞清楚这2者之间的关系, 就需要我们从 new关键字和构造函数说起, 通过new创建对象,做了下面的步奏:

  1. 在内存中创建一个空对象
  2. 这个对象的的__prpto__属性会被赋值为该构造函数的prototype属性
  3. 构造函数内部的this,会指向创建出来的空对象
  4. 执行函数内的代码
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象 this
js 复制代码
 //自定义构造函数创建对象
  function Person(){
      //模拟 new关键字 (模拟代码)
      var obj = {}
      this = obj
      this.__proto__ = Person.prototype
      return this
  }
 

从而我们可以知道通过 构造函数创建的实例对象(instance)的原型__proto__ 构造函数的原型prototype是相同的

js 复制代码
  //构造函数
  function Foo(){}
  //实例对象
  var f = new Foo()
  
  console.log(f.__proto__ === Foo.prototype) // true
2.2函数,函数原型,实例对象三者的关系

首先给出结论:

  1. 每个函数都有一个默认的属性叫做:prototype, prorotype属性保存一个对象, 这个对象我们称为原型对象
  2. 每个原型对象中都有一个默认的属性constructor,这个constructor属性指向当前原型对象对应的 '构造函数'
  3. 通过构造函数创建出来的对象 我们称为:实例对象, 每个实例对象中都有一个 __proto__属性, 这个__proto__属性指向创建它的那个 构造函数的原型对象prototype
js 复制代码
function Foo() {}

//获取prototype原型对应的对象
console.log(Foo.prototype);

//从上面的打印结果 我们可以看到 prototype原型对象 有一个constructor属性,这个constructor属性 指向的是 Foo函数
console.log(Foo.prototype.constructor === Foo) //true

//实例对象
var f1 = new Foo();
//实例对象的__proto__属性 指向 构造函数Foo的原型对象
console.log(f1.__proto__ === Foo.prototype); //true

3者关系的展示图

2.3原型共享方法

其实我们通过构造函数创建多个实例对象的时候, 如果在构造函数中定义方法,其实是有一定的问题的,下面我们观察一下是什么问题

js 复制代码
//构造函数
function Student(name){
    this.name = name
    this.study = function(){
        console.log(`${this.name}好好学习`);
    }
}

//创建实例对象
 var s1 = new Student("MM");
 console.log(s1.name); // MM
 s1.study(); //

 var s2 = new Student("TT");
 console.log(s2.name); //TT
 s2.study()
 //说明2个实例的study方法不是同一个方法
 console.log(s1.study === s2.study); // false

问题: 如果我们创建多个对象, 例如:创建100个或者更多的实例对象, 那么就会在内存中创建 100个study方法,这样就会浪费很多内存。 举个例子:学校有100个学生, 不能给100个学生配置100个教室用来学习, 共享一个教室就可以用来学习了
那么我们就会用到原型对象,由于实例对象的原型__proro__ 和 函数原型prototype的关系,所以在函数原型protorype的对象上 设置study方式,是可以共用共享的, 因为所有创建出来的实例对象的原型__proto__都会指向函数的prototype原型 可以参见上面的关系展示图

js 复制代码
function Student(name){
    this.name = name
}

//给原型对象添加方法
Student.prototype.study =  function(){
    console.log(`${this.name}好好学习`);
}
 var s1 = new Student("MM");
 console.log(s1.name);
 s1.study()

 var s2 = new Student("TT");
 console.log(s2.name);
 s2.study()
 console.log(s1.study === s2.study); // true
2.4自定义原型对象
js 复制代码
//构造函数
function Person() {}

//我们知道 原型对象有一个constructor属性,指向构造函数
//所以 自定义构造函数需要添加constructor
Person.prototype = {
    //这样指定的缺点:是可以获取到胡乱添加数据属性
    // constructor:Person,
    msg:'你好啊',
    say:function(){
        console.log('你好啊');
    }
}

//所以通过defineProperty添加属性比较合适
Object.defineProperty(Person.prototype,'constructor',{
    value: Person,
    configurable:true,
    enumerable:false,
    writable:true
})

//打印原型对象
console.log(Person.prototype);

//看看关系
console.log(Person.prototype.constructor === Person); //true
2.5原型链

我们知道,在对象(obj)上获取属性,如果在当前对象(obj)中没有获取到,就会去它的原型上去查找获取

js 复制代码
//创建一个对象
var obj = {
    name:'lee',
    age: 18
}

//相当于执行了
// var obj = new Object();
//前面我们已经知道 对象__proto__ 和 函数prototype原型及 实例对象的关系

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

//如果我们访问 message属性
console.log(obj.message)

//那么就会执行下面的查找操作
//1.现在obj对象中获取message
//2.如果没有。 去__proto__原型对应的原型对象查找
//3.我们知道 __proto__是函数 protorype的对象的指向, prototype对象的原型也是有原型对象的,所以也会去查找

//这个查找的对应原型的链条我们称为原型链

结果 查找顺序

  1. obj上面查找
  2. obj.__proto__ 上面查找
  3. obj.__proto__.__proto__ -> null上面查找
  4. obj.__proto__是指向Object.prototype原型对象, 原型对象也是对象, 也有自己的__proto__属性 指向null
    1.对象中__proto__组成的链条我们称之为原型链

2.对象在查找属性和方法的时候, 会先在当前对象查找

如果当前对象中找不到想要的, 会依次去上一级原型对象中查找

如果找到Object原型对象都没有找到, 就会返回undefined

从上面的obj.__proto__.__proto__打印中我们可以知道(下图),这个原型对象的的构造函数是 Object,这个对象的原型__proto__指向是null

三.继承

我们知道面向对象编程, 对象的3大特性: 封装,多态,继承

封装: 就是把属性和方法 封装到一个类中

继承: 把一些重复的代码和逻辑抽取到父类, 方便子类使用
先看一段代码, 从下面的代码中, 我们可以发现其实有很多属性和方法是可以共用的,比如: name, age属性 和 一些 running,eating方法

继承:我们可以把 name,age属性和running,eating方法抽取到 Person类里面, 让 Student和Teacher继承上面的属性和方法, 并且Student特有的属性和方法 还保持在自己类里面

js 复制代码
function Person(name, age){
    this.name = name
    this.age = age
}
//给 Person实例对象 共享的方法
Person.prototype.running = function(){}
Person.prototype.eating = function(){}



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

Student.prototype.running = function(){}
Student.prototype.eating = function(){}
Student.prototype.studing = function(){}


function Tearcher(name,age,title) {
    this.name = name;
    this.age = age;
    this.title = title;
}

Tearcher.prototype.running = function(){}
Tearcher.prototype.eating = function(){}
Tearcher.prototype.teach = function(){}

1.实现继承的方式一(原型链继承)

前面我们了解了什么事原型链, 那么我们如果通过原型链实现继承

js 复制代码
function  Person(){
    this.name = "lee"
    this.friends = ["TT"]
}
Person.prototype.eating = function(){
    console.log(this.name, '吃东西');
}



//子类
function Student(){
    this.name = 'stu123';
    this.sno = '789';
}


var p = new Person();
//让Student的原型对象指向p实例对象,方便stu实例对象通过原型查找,访问
Student.prototype = p;

//学生特有的方法
Student.prototype.studying = function(){
    console.log('学习');
}

var stu = new Student();
stu.studying(); // 学习
stu.eating(); //stu123 吃东西
stu.friends.push("MM")
p.eating(); //lee 吃东西

从上面的代码中我们可以发现

1.共享了p实例的属性 stu可以修改访问

2.不能给Person传递属性,可以共享给stu
下面是原型链继承的指向图

2.借用构造函数继承

借用构造函数就是为了解决 原型链继承的问题,我们通过pply()和call()方法来实现,其实这个2个方法还蛮重要, 其中this的指向问题就可以这个2个方法解决

js 复制代码
//父类
function Person(name, age){
    this.name = name;
    this.age =  age;
}

Person.prototype.run = function(){
    console.log(this.name + "跑步");
}

//借用构造函数 主要用于继承属性
function Student(name,age,sno){
    //借用构造函数调用, 更改this的指向
    Person.call(this,name,age);
    //学号
    this.sno = sno;
}


var stu = new Student('Lee',15,788889);

console.log(stu);
console.log(stu.name);

// stu.run() //报错 方法找不到

缺点: 不能继承使用原型的属性和方法

3.组合式继承

组合式继承主要是 上面2中方式的结合

js 复制代码
 //组合继承

 function Person(name, age){
  this.name = name
  this.age = age
 }

 Person.prototype.running = function(){
  console.log(this.name + "跑步");
 }



 const p = new Person("kkk",19)

 console.log(p);
 console.log(p.__proto__);


 function Student(name, age, sno){
  Person.call(this, name, age)
  this.sno = sno
 }

 Student.prototype = new Person()


 Student.prototype.studying = function(){
  console.log(this.name + "学习");
 }

 const s1 = new Student("lee", 18,1001);
 console.log(s1);
 s1.running()
 s1.studying()

 const s2 = new Student("礼拜", 20, 10002)
 console.log(s2);
 s2.running()
 s2.studying()

 console.log("===============");

 console.log(s1.__proto__);

从上面代码的执行中 我们可以发现,Person函数会被执行2次,并且我们可以从下面的图中发现, Student的原型对象p中 有2个共享的name和age属性

4.寄生组合式继承

从上面的继承中,其实还可以在优化,我们发现在指定原型对象的时候,我们使用了父类的 构造函数创建一个p的实例对象,实际中我们只要满足:

1.创建一个实例对象instance

2.这个对象instance的隐式原型__proto__指向父类的显示原型prototype;

3.把对象赋值给子类的显示原型ptototype

js 复制代码
function inherrt(Subtype,SuperType){
    //Object.create方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型
    // Subtype.prototype = Object.create(SuperType.prototype);
    //利用空对象  没有任何问题
    Subtype.prototype = creatObject(SuperType.prototype);
    Object.defineProperty(Subtype.prototype,'constructor',{
        value: Subtype,
        configurable:true,
        enumerable:false,
        writable:true
    });
}


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

//继承的实现

//父类
function Person(name,age){
    this.name = name;
    this.age = age;
}

Person.prototype.running = function(){
    console.log('跑步')
}
Person.prototype.eating = function(){
    console.log('吃饭')
}

//子类
function Student(name,age,sno){
    Person.call(this,name,age);
    this.sno = sno;
}

//实现方法继承
inherrt(Student,Person);

Student.prototype.studying = function(){
    console.log('学习')
}

var s1 = new Student('lee',18,999000);
s1.running();
s1.eating();
s1.studying();
console.log(s1.name);
console.log(s1.sno);

console.log(s1);

console.log("-=================");

const p = new Person("mm", 19)

console.log(p);
相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰10 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪10 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪10 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy11 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom11 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom12 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom12 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom12 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试