深入JS面向对象(原型-继承)(三)

简介: 深入JS面向对象(原型-继承)

深入JS面向对象(原型-继承)(二)https://developer.aliyun.com/article/1470348

Person构造函数原型内存图

image.png

image.png

image.png


//上面为内存图表现,p1对象跟p2对象的__proto__隔壁那个空格是指内存地址
function Person(){
    
}
var p1 = new Person()
var p2 = new Person()
//函数的执行体创建p1的对象,还有p2的对象,这个是我们一开始就通过var p1 = new Person()还有var p2 = new Person()达成的,这个对象`new Person`是构造函数,前面我们已经学习过了,然后他们身上是有隐式原型__proto__的,而隐式原型是会指向Person函数的原型对象的显示原型prototype。总结就是p1跟p2指向了构造函数,构造函数的隐式原型指向了函数的显示原型。这也就造就了下面两个true相等的原因
console.log(p1.__proto__ === Person.prototype);//true
console.log(p2.__proto__ === Person.prototype);//true

赋值为新的对象

image.png

function Person(){
}
var p1 = new Person()
var p2 = new Person()
//想要取到对象中没有的属性的办法:可以在原型中直接加上去,那在p1中找不到就会去原型上找,然后就可以找到了
p1.__proto__.name = "小余"
console.log(p1.name);//小余
//但其实还有更好的办法,我们可以直接放在函数Person的显示原型里面,然后按照顺序,p1里面找不到就去p1自己的隐式原型__proto__找,如果也找不到就会继续往上追溯到Person的显式原型prototype中找,那这回可算找到了
Person.prototype.name = "XiaoYu"
console.log(p1.name);//XiaoYu
//如果我们对p2的隐式原型进行修改,一样会作用到p1身上,这是为什么?
//规范回答:new操作符导致的:这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype
//理解回答:因为p2的隐式原型会指向Person的显式原型prototype,而p1最终也是会指向到Person的显式原型,所以他们会找到同一个地方,所以就会导致p2隐式原型的改动也会影响到p1隐式原型的改动,因为他们最终追溯到的都是一样的地方
p2.__proto__.name = "大满"
console.log(p1.name);//大满

函数原型上的属性

prototype添加属性

image.png

constructor属性

  • 事实上原型对象上面是有一个属性的:constructor
  • 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;
function foo(){
}
console.log(foo.prototype,"纯foo.prototype打印")//这个打印出来是个空对象{},但是事实上这个并不是空的,只是因为可枚举属性被设置为了false
console.log(Object.getOwnPropertyDescriptors(foo.prototype),"getOwnPropertyDescriptors打印foo.prototype");

image.png

//我们知道是事实上这个并不是空的,只是因为可枚举属性被设置为了false,那就可以采取另一种方式,将他的枚举属性设置为true,那就可以看到了
function foo(){
}
Object.defineProperty(foo.prototype,"constructor",{
    enumerable:true,
    configurable:true,
    writable:true,
    value:"小余今天抓住了一只小满"
})
console.log(foo.prototype.constructor);//能够打印出来constructor属性了
//但如果将上面defineProperty给注释掉的话,foo.prototype.constructor就会打印出另一个结果:[Function: foo]
//prototype.constructor = 构造函数本身,也就是foo函数
//我们来验证一下这句话:prototype.constructor = 构造函数本身,也就是foo函数
function foo(){
}
console.log(foo.prototype.constructor.name);//foo
//我们知道构造函数本身都是有名字的,通过在foo.prototype.constructor,也就是构造函数本身的基础上,打印了name,果然出来了foo这个名字
  • 所以来个总结:
  • 在原本各种指向的基础上,在我们追溯到显式函数身上,也就是上面原型图中的Person的原型对象prototype之后,由Person原型对象身上的constructor又指回了foo这个构造函数本身。这样就形成了一个完美的循环
//我们知道这里形成了循环后,我们甚至能做出来一些骚操作
function foo(){
}
console.log(foo.prototype.constructor.prototype.constructor.prototype.constructor.name) //foo
//我们让他们两者之间不断的互相循环,最终又回到的构造函数本身,然后取出来了构造函数的名字

重写原型对象

这个互相引用是会回收的,因为JS的垃圾回收机制是标记清除,是从根节点开始看有没有引用的

image.png

当我们使用重写原型方法之后,也就是在内存里又创建了一个对象,内存图如下(代码在下面的代码块中):

  • foo函数不再指向它的原型对象了,而是指向新的对象了,刚指向的时候,这个新对象连constructor都没有
  • 指向新对象之后,foo函数的原型对象就会被销毁掉了,因为我们js的垃圾回收机制是采用标记清除法(详细的内容往回翻)

image.png

当我们填入内容之后:

image.png

  • 如果我们需要在原型上添加过多的属性,通常我们会重新整个原型对象:
//没有重写之前
Person.prototype.name = "小余"
Person.prototype.age = 20
Person.prototype.learn = function(){
    console.log(this.name+"在学习");
}
function Person(){
}
//重写之后:修改之后是不是简洁很多,去掉了很多重复的元素了
Person.prototype = {//这种对象形式的写法意味着直接在内存里创建了一个新的对象
    name:"小余",
    age:20,
    learn:function(){ 
        console.log(this.name+"在学习");
    }
}
  • 前面我们说过, 每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性;
  • 而我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函 数, 而不是Person构造函数了

image.png

function Person(){
}
Person.prototype = {
    name:"小余",
    age:18,
  height:1.88
}
//注意多出来的这两个步骤
var f1 = new Person()
console.log(f1.name+"今年"+f1.age);//小余今年18,这里能够打印出来,但是最终指向的地方已经变成新的对象了(原因是因为我们重写了原型对象,通俗的说就是我们在prototype不再一个个等于慢慢写,而是直接使用对象的形式,省去了重复的Person.prototype.xxx = xx,这样的效果就刚刚上面说的一样,相当于在内存中创建了新的对象,构造函数Person的指向也就跟着发生了变化),不再是Person的显式原型了

这时候问题就来了,那原来的就这么不要了吗?根据上面的图,我们如果想抛弃原来的那个显式原型的话,是不是还缺少了点什么?


缺少了constructor啦,就是构造函数的标志,所以我们只要给新对象加上constructor就完工了,原来那个显式原型就可有可无了

image.png

function Person(){
}
Person.prototype = {
    //constructor:Person,//注意这里,这里是我们添加上来的,但是跟原版仍然有点区别,那就是原版的enumerable是为false的,而这样添加的enumerable为true,也就是可枚举的。所以真实开发中我们一般不这么添加,真实开发的添加方法我放在下面了,也就是我们刚刚所学的方式,能够解决我们通过目前这种方式添加时enumerable为true的问题
    name:"小余",
    age:18,
  height:1.88
}
var f1 = new Person()
//真实开发中我们通过Object.defineProperty方式添加constructor
Object.defineProperty(Person.prototype,"constructor",{
    enumerable:false,
    writable:true,
    configurable:true,
    value:Person
})
console.log(f1.name+"今年"+f1.age);

创建对象 – 构造函数和原型组合

  • 我们在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如running、eating这些函数
  • 那么有没有办法让所有的对象去共享这些函数呢?
  • 可以,将这些函数放到Person.prototype的对象上即可;
//错误写法
function Person(name,age,sex,address){
    Person.prototype.name = name,
    Person.prototype.age = age,
    Person.prototype.sex = sex,
    Person.prototype.address = address
}
var p1 = new Person("小余",18,"男","福建")
console.log(p1.name);//小余
var p2 = new Person("小满",24,"男","乔家大院")
console.log(p1.name);//小满
//没错,这是错误写法,不能够这么写,因为我们在Person其实会创建一个空对象,然后绑定在this身上调用返回(内部实现,看不到的),我们往p1,p2传入数据,第一时间肯定是先去p1跟p2各自的身上找,但很显然,我们在Person里面的代码,是直接放到他的显式原型上面了,而Person本身就什么都没有,所有当p1的数据放到显式原型上后,p2的数据紧随其后跟着放上去了,就会直接在显式原型中直接覆盖掉p1的数据。当我们使用p1.name的时候,本身找不到,紧接着去隐式原型中找,没找到,再去显式原型中找,这次找到了,但是找到的是被p2覆盖掉的数据,所有当我们p1.name拿出来的时候就会是p2的name数据
//正确写法
function Person(name,age,sex,address){
    this.name = name,
    this.age = age,
    this.sex = sex,
    this.address = address
    // this.eating = function(){
    //     console.log(this.name+"今天吃烤地瓜了");
    // }
}
//由于函数如果放在Person里面,那每次都会在构造函数中创建出一个新的,但是里面的内容其实都是一样的,所以最好的方式就是放在原型中,需要的时候顺着原型链找过去
Person.prototype.eating = function(){
    console.log(this.name+"今天吃烤地瓜了");
}
Person.prototype.running = function(){
    console.log(this.name+"今天跑了五公里");
}
var p1 = new Person("小余",18,"男","福建")
var p2 = new Person("小满",24,"男","乔家大院")
console.log(p1.name);//小余 不会发生覆盖的问题了

内容补充(可枚举)

可枚举属性的补充


  • 我们设置可枚举为false,在node环境下是打印不出address地址的,但是在浏览器是会显示出来的,这是浏览器为了更加方便我们进行调试,所以把不可枚举的属性也展示出来了,在coderwhy老师的苹果电脑的谷歌浏览器中展示出来的不可枚举属性是会半透明显示的(提示开发者这是一个不可枚举的属性),但是我在window电脑上的Edge浏览器中显示出来的不可枚举属性是没有半透明效果的,就正常显示
var obj = {
    name:"小余",
    age:20
}
Object.defineProperty(obj,"address",{
    enumerable:false,//默认false 
    value:"福建省"
})
console.log(obj);

JavaScript中的类和对象

  • 当我们编写如下代码的时候,我们会如何来称呼这个Person呢?
  • 在JS中Person应该被称之为是一个构造函数;
  • 从很多面向对象语言过来的开发者,也习惯称之为类,因为类可以帮助我们创建出来对象p1、p2;
  • 如果从面向对象的编程范式角度来看,Person确实是可以称之为类的;
function Person(){
    
}
var p1 = new Person()//通过了new调用,Person变为构造函数。生成新对象,由p1接收
var p2 = new Person()//但也可以称为 类,在ES6之后开始可以使用class去定义,但本质上还是通过原型、原型链 面向对象封装、继承,class它只是一个语法糖而已

知识点补充:语法糖是什么?


  • 语法糖是程序设计语言中一种语法上的简化,它可以使程序员在编写程序时使用更简洁、更易读的语法,同时编译器或解释器会将其转换为更底层的语法。语法糖并不会影响程序的功能或性能,只是提供了一种更方便的编码方式
  • 在 JavaScript 中,一个常见的语法糖是箭头函数。箭头函数的语法简洁,可以使代码更简洁易读。下面是一个使用传统函数定义方式和使用箭头函数定义方式实现同样功能的例子。
  • 可以看出在使用箭头函数时,省去了函数的名称、return关键字,并且在参数比较简单的情况下可以省去括号,使代码更简洁易读。


// 使用传统函数
let numbers = [1, 2, 3, 4, 5];
let double = numbers.map(function(number) {
  return number * 2;
});
console.log(double); // [2, 4, 6, 8, 10]
// 使用箭头函数
let numbers = [1, 2, 3, 4, 5];
let double = numbers.map(number => number * 2);
console.log(double); // [2, 4, 6, 8, 10]


面向对象的特性 – 继承

  • 面向对象有三大特性:封装、继承、多态
  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
  • 多态:不同的对象在执行时表现出不同的形态;
  • 这里核心讲述其中的继承
  • 那么继承是做什么呢?
  • 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。
  • 通俗的来说就是重复利用一些代码(对代码的复用)
  • 继承是多态的前提
  • 那么JavaScript当中如何实现继承呢?
  • 不着急,我们先来看一下JavaScript原型链的机制;
  • 再利用原型链的机制实现一下继承;

JavaScript原型链

  • 在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。
  • 我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:

image.png

var obj = {
    name:"小余",
    age:20
}
obj.__proto__ = {
}
obj.__proto__.__proto__ = {
}
obj.__proto__.__proto__.__proto__ = {
    address:"福建省"
}
//[[get]]操作
//1.在当前的对象里面查找属性
//2.如果没有找到,这个时候会去原型对象[[__proto__]]上查找
console.log(obj.address);

Object的原型

  • 那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型proto属性呢?
console.log(obj.__proto__.__proto__.__proto__.__proto__);//[Object: null prototype] {}
//到底是找到哪一层对象之后停止继续查找了呢?
//如果每个原型后面还有原型,那不就无穷无尽吗?但显然是不可能的,原型链的尽头就是Object原型,位于我们__proto__的下一层,你本身自带一个__proto__,在prototype里面,这个__proto__打开是js替我们实现的原型,再下一层就是Object的原型了,也就是最后一层,但是如果你在自身的身上继续叠加__proto__的话,那原型链的尽头就会在这个基础上继续加,加深几层取决于你又套了几层__proto__


  • 我们会发现它打印的是 [Object: null prototype] {}
  • 事实上这个原型就是我们最顶层的原型了
  • 从Object直接创建出来的对象的原型都是 [Object: null prototype] {}
  • 那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?
  • 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
  • 特殊二:该对象上有很多默认的属性和方法;

Object顶层原型来自哪里

image.png

image.png

image.png

//创建了一个对象
var obj = {}
//创建了一个对象,相当于obj对象字面量的语法糖
var obj2 = new Object()//obj2.__proto__ = Object.prototype
function Person(){
}
//new出构造函数这个操作发生的步骤:
//1.在内存中创建一个对象,var moni = {}
//2.this的赋值,this = moni
//3.将Person函数的显示原型prototype赋值给前面创建出来的对象的隐式原型,moni.__proto__=Person.prototype
//4.执行代码体
//5.返回这个对象
var p = new Person()
//但是当我们在使用语法糖new Object()的时候,赋值的情况如下:
//var moni = {}
//this = moni
//moni.__proto__ = Object.prototype
//obj2.__proto__ = Object.prototype
//那这时候就证明了一点,obj.__protot__ === Object.prototype
var obj = {
    name:"xiaoyu",
    age:18
}
//obj.__proto__等价于Object.prototype  Object是所有类的父类 
console.log(obj.__proto__)//[Object: null prototype] {}
console.log(Object.prototype);//[Object: null prototype] {}
console.log(obj.__proto__ === Object.prototype);//true
//Object.prototype里面的东西是很多的,只是因为都是不可枚举的,所以在node环境下打印是看不见的,但是你可以打印在浏览器的控制台,这里是能看见的,就刚刚说的那样,浏览器为了方便开发者调试,所以会将不可枚举的属性也显示出来
//我们发现Object的原型对象身上也是有构造函数constructor的
console.log(Object.constructor);//[Function: Function]
//打印出Object的原型对象身上的所有属性,记得加上s 别打错啦
console.log(Object.getOwnPropertyDescriptors(Object.prototype))
//打印对象身上显式原型的隐式原型
console.log(Object.prototype.__proto__);//null,因为Object身上的原型已经是最后一层了,属于最顶层的原型,继续往下找只有null

原型对象跟原型属性的关系(自我总结)

JavaScript 中所有对象都是通过构造函数创建的,并且对象都有一个隐式的属性,称为原型 (prototype)。每一个构造函数都有一个prototype属性,这个属性指向一个对象,这个对象也叫原型对象,继承了一些属性和方法。


每当一个新对象被创建,它的内部指针就会指向它的构造函数的原型对象。这意味着如果我们在原型对象上修改了一个属性或方法,那么所有继承了这个原型对象的对象都会受到影响。



可以看出构造函数的原型对象的属性是被所有继承了它的对象共享的,而对象的属性是被对象本身独享的。


原型属性可以用来实现继承

// 定义一个构造函数Person
function Person(name) {
 this.name = name;
}
// 添加一个原型属性 sayName
Person.prototype.sayName = function() {
 console.log(`My name is ${this.name}`);
}
// 创建一个对象 person1
let person1 = new Person("John");
console.log(person1.name); // "John"
console.log(person1.sayName); // "My name is John"
// 修改原型上的属性,所有继承原型的对象会受影响
Person.prototype.sayName = function() {
 console.log(`Hello, My name is ${this.name}`);
}
console.log(person1.sayName); // "Hello, My name is John"

Person构造函数原型

function Person(){
}
console.log(Person.prototype);//{},有时候看着是空的未必真的是空的
console.log(Object.getOwnPropertyDescriptors(Person.prototype))
// {
//     constructor: {
//       value: [Function: Person],
//       writable: true,
//       enumerable: false,
//       configurable: true
//     }
// }

创建Object对象的内存图

image.png

原型链关系的内存图

image.png


深入JS面向对象(原型-继承)(四)https://developer.aliyun.com/article/1470352

目录
相关文章
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
31 0
|
5天前
|
JavaScript 前端开发 安全
原型与继承
原型与继承
6 0
|
设计模式 JavaScript 前端开发
深入JS面向对象(原型-继承)(二)
深入JS面向对象(原型-继承)
44 0
|
30天前
|
JSON JavaScript 前端开发
深入JS面向对象(原型-继承)(四)
深入JS面向对象(原型-继承)
27 0
|
1月前
|
JavaScript 前端开发
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
55 0
原型链继承: 原理:将父类的实例作为子类的原型
原型链继承: 原理:将父类的实例作为子类的原型
|
3月前
|
设计模式 前端开发 JavaScript
【面试题】 对象、原型、原型链与继承?这次我懂了!
【面试题】 对象、原型、原型链与继承?这次我懂了!
|
3月前
|
设计模式 前端开发 JavaScript
【面试题】对象、原型、原型链与继承 ,你了解多少?
【面试题】对象、原型、原型链与继承 ,你了解多少?
|
前端开发 JavaScript
原型、原型链和继承~我终于搞定了啊~~
不管是在学习JavaScript,还是面试找工作,逃不过去的就是原型、原型链和继承这几个重点和难点;对于这些内容的掌握还是很有必要的。
92 0
原型、原型链和继承~我终于搞定了啊~~