深入对象的原型和继承

一.创建对象的方式

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);
相关推荐
m0_7482309424 分钟前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_5895681032 分钟前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
黑客老陈2 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安2 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
暴富的Tdy2 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se2 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235612 小时前
web 渗透学习指南——初学者防入狱篇
前端
℘团子এ2 小时前
js和html中,将Excel文件渲染在页面上
javascript·html·excel