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

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

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

对象方法补充

  • 获取对象的属性描述符:
  • getOwnPropertyDescriptor
  • getOwnPropertyDescriptors
  • 禁止对象扩展新属性:preventExtensions
  • 给一个对象添加新的属性会失败(在严格模式下会报错)
  • 密封对象,不允许配置和删除属性:seal
  • 实际是调用preventExtensions
  • 并且将现有属性的configurable:false
  • 冻结对象,不允许修改现有属性:freeze
  • 实际上是调用seal
  • 并且将现有属性的writable:false
var obj = {
    _age:20,
}
Object.defineProperties(obj,{
    name:{
        //是否可配置
        configurable:true,
        //是否枚举
        enumerable:true,
        //添加新值
        value:"小余",
        //是否可写入
        writable:true
    },
    age:{
        configurable:false,
        enumerable:false,
        get:function(){
            return this._age
        },
        set:function(value){
            this._age = value
        }
    }
})
//获取某一个特征属性的属性描述符
console.log(Object.getOwnPropertyDescriptor(obj,'name'));
//{ value: '小余', writable: true, enumerable: true, configurable: true }这个就是name的配置了
console.log(Object.getOwnPropertyDescriptor(obj,'age'));
//{
//  get: [Function: get],
//  set: [Function: set],
//  enumerable: false,
//  configurable: false
//}
//获取对象的所有属性描述符
console.log(Object.getOwnPropertyDescriptors(obj));//请注意这个的区别跟上面那个最后多了一个s
// {
//     _age: { value: 20, writable: true, enumerable: true, configurable: true },
//     name: { value: '小余', writable: true, enumerable: true, configurable: true },
//     age: {
//       get: [Function: get],
//       set: [Function: set],
//       enumerable: false,
//       configurable: false
//     }
// }

Object的方法对对象的限制

//禁止对象继续添加新的属性
var obj = {
    _age:20,
}
Object.preventExtensions(obj)
obj.name = "小余"
obj.sex = "男"
console.log(obj);//{ _age: 20 },阻止添加成功
//禁止对象配置/删除里面的属性
//麻烦的方式:
var obj = {
    _age:20,
    name:"小余"
}
for(var key in obj){
    Object.defineProperty(obj,key,{
        //将每一个属性都设置为不可配置,包括禁止删除里面的属性
        configurable:false,
        value:key
    })
}
console.log(Object.getOwnPropertyDescriptors(obj));//自己打印出来看看结果,其中的configurable确实都为false了
//简单的方式:
Object.seal(obj)//尝试将这行代码注释掉,看前后对比
//验证方式1:
delete obj.name
console.log(obj.name);
//验证方式2:
console.log(Object.getOwnPropertyDescriptors(obj));
//让属性不可以修改(相当于让writablel:false)
var obj = {
    //私有属性(js中没有严格意义上的私有属性,你依旧可以通过obj._age访问到,但是外人是不知道这个隐藏起来的属性,只知道他的替代obj.age)
    _age:20,
    name:"小余"
}
Object.freeze(obj)
obj.name = "小满zs"
console.log(obj);//{ _age: 20, name: '小余' },name没有被修改为"小满zs"

创建多个对象的方案

  • 如果我们现在希望创建一系列的对象:比如Person对象
  • 包括张三、李四、王五、李雷等等,他们的信息各不相同;
  • 那么采用什么方式来创建比较好呢?
  • 目前我们已经学习了两种方式:
  • new Object方式;
  • 字面量创建的方式;
var p1 = {
    name:"小余",
    age:20,
    sex:"男",
    address:"福建",
    eating:function(){
        console.log(this.name+"在吃烧烤");
    },
    running:function(){
        console.log(this.name+"在跑步做运动");
    }
}
var p2 = {
    name:"小满",
    age:23,
    sex:"男",
    address:"北京",
    learn:function(){
        console.log(this.name+"在学编程");
    },
    running:function(){
        console.log(this.name+"在跑步做运动");
    }
}
var p3 = {
    name:"洛洛",
    age:20,
    sex:"女",
    address:"福建",
    learn:function(){
        console.log(this.name+"在学Go语言跟rust语言");
    },
    running:function(){
        console.log(this.name+"在内卷");
    }
}
//以上的方式过于相似,存在过于重复的代码,我们能不能进行优化呢,用另一种方式创建?
  • 上面这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码;

创建对象的方案 – 工厂模式

  • 我们可以想到的一种创建对象的方式:工厂模式
  • 工厂模式其实是一种常见的设计模式;
  • 通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象;
function createPerson(){
}
//我们虽然里面属性大多数都是相同的,但是数据是不一样的,比如p1的name是小余,p2的是小满,p3的是洛洛,这个时候我们就可以向调用的里面传入参数来实现我们不同数据的传输
var p1 = createPerson()
var p2 = createPerson()
var p3 = createPerson()
//上面那个空模板的改善方式,传入参数
function createPerson(name,age,sex,occupation,address){
  var p = new Object()
    p.name = name
    p.age = age
    p.sex = sex
    p.occupation = occupation
    p.address = address
    p.eating = function(){
        console.log(this.name + "在吃满汉全席")
    }
    return p
}
var p1 = createPerson("小余",20,"男","大二学生","福建",)
var p2 = createPerson("小满",24,"男","京东程序员","北京")
var p3 = createPerson("洛洛",20,"萌妹子","Go+Rust+JavaScript+Node.js全栈工程师兼小余的同学",'福建')
console.log(p1,p2,p3);
//打印效果如下:
// {
//     name: '小余',
//     age: 20,
//     sex: '男',
//     occupation: '大二学生',
//     address: '福建',
//     eating: [Function (anonymous)]
//   } {
//     name: '小满',
//     age: 24,
//     sex: '男',
//     occupation: '京东程序员',
//     address: '北京',
//     eating: [Function (anonymous)]
//   } {
//     name: '洛洛',
//     age: 20,
//     sex: '萌妹子',
//     occupation: 'Go+Rust+JavaScript+Node.js全栈工程师兼小余的同学',
//     address: '福建',
//     eating: [Function (anonymous)]
//   }

image.png

工厂函数的缺点

1. 通过上述console.log(p1,p2,p3)打印出来的是我们的字面量
2. 缺少对象应该有的类型,只能看到是Prototype那里的类型都是Object,这种形容过于宽广,分类不够具体。因为人是动物,猫咪也是动物,但两者是不能够混为一谈的。获取不到对象最真实的类型
3. 我们想要达到的效果是:当我们拿到p1、p2、p3的时候,我们还能够知道他们对应的类型,知道这是由什么产生的,而这个是工厂函数没办法做到的事情
//如果你对上面的类型还一知半解,也可以参考我的想法,我认为这是分类过于模糊,我们通过工厂函数创建出来了一个"人",他身上的属性有姓名,身高,职业,性别等等,我们通过工厂函数传入参数也只是对数据进行改变,他本质上的属性是一样的,脱离不了人本身,最多一个叫小余,另一个叫小满,两个不一样的人,但都是人。这个时候我们希望调用工厂函数这个函数的时候,告诉我们类型是人,具体一些,而不是"宇宙中存在的东西",那太过于宽广,实在是跟没说一样,毕竟你抽象出来的东西哪个不是宇宙中的东西对吧

认识构造函数

  • 工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型
  • 但是从某些角度来说,这些对象应该有一个他们共同的类型;
  • 下面我们来看一下另外一种模式:构造函数的方式;
  • 我们先理解什么是构造函数?
  • 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;
  • 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;
  • 但是JavaScript中的构造函数有点不太一样;
  • JavaScript中的构造函数是怎么样的?
  • 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
  • 那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;
  • 那么被new调用有什么特殊的呢?

new操作符调用的作用

  • 如果一个函数被使用new操作符调用了,那么它会执行如下操作:
  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行函数的内部代码(函数体代码);
function foo(){
    var moni = {}
    this.moni
}
//new foo相当于会默认执行上面这两个步骤(平时是不显示的,内部自己执行的),然后进行我们的第五步返回创建出来的新对象,不需要我们手写return返回值


  1. 如果构造函数没有返回非空对象,则返回创建出来的新对象;
function foo(){
    console.log("foo~");
}
var f1 = new foo//foo~
console.log(f1);//foo {} 确实创建出来的一个空对象,且类型就是foo
//很明显类型精准了很多,如果上面不够明显,我们可以再来一个案例(输出xiaoyu {},已经很能证明了吧)
function xiaoyu(){
    console.log("我是小余");
}
var f1 = new xiaoyu//我是小余
console.log(f1);//xiaoyu {},node打印结果


  • 浏览器控制台打印结果:

image.png

function foo(){
    console.log("foo~");
}
//foo就是一个普通的函数
foo()
//换一种方式来调用foo函数,使用操作符new
new foo()//一旦我们通过new这样调用,那么这个函数就是一个构造函数了
new foo //我们甚至可以不写这个小括号
//上面3处调用的结果都是foo~,全部成功调用。请注意我说的是3处,也就是上面第四点说的:执行函数的内部代码(函数体代码)
//当我们通过new去调用一个函数时,和普通的调用到底有什么区别?
函数什么时候return

这里临时补充一个知识点,突然模糊了什么时候在函数中什么时候要写return?前面有说过,但是好像没写上,这里补一下:


  • 在JavaScript中,当你想要从一个函数中返回一个值的时候,就可以使用return关键字。
    这里的return的意义是结束函数的执行,并将指定的值作为结果返回给调用函数的代码。例如,你可以这样定义一个函数:
  • 这个函数接受两个参数,并返回它们的和。你可以调用这个函数,并将返回值赋值给一个变量,例如:
  • 在函数内部,你可以使用多个return语句,但是只有第一个会被执行,因为一旦执行了return语句,函数就会立即结束。
    如果你希望函数执行完所有的代码后再返回一个值,你可以在函数的最后一行不指定任何返回值,这样函数就会返回一个特殊的值undefined
    举个例子,假设你有一个函数,它接受一个数字并返回它的平方:
function add(a, b) {
  return a + b;
}


let sum = add(1, 2); // sum的值为3


function square(x) {
  console.log(x * x);
}
let result = square(2); // 输出4,result的值为undefined

在这个函数中,我们没有使用return语句来明确地返回一个值,所以函数会返回undefined

创建对象的方案 – 构造函数

  • 我们来通过构造函数实现一下:
function xiaoyu(name,age,sex,address){
    this.name = name
    this.age = age
    this.sex = sex
    this.address = address
    this.eating = function(){
        console.log(this.name + "在吃鱿鱼须");
    }
    this.runding = function(){
        console.log(this.name + "在跟坤坤打篮球");
    }
}
var f1 = new xiaoyu("小余同学",20,"男","福建")
console.log(f1);
// xiaoyu {
//     name: '小余同学',
//     age: 20,
//     sex: '男',
//     address: '福建',
//     eating: [Function (anonymous)],
//     runding: [Function (anonymous)]
//   }
//然后跟工厂函数一样的,我们可以进行重复描写
var f2 = new xiaoyu("小满zs",23,"男","北京")
var f3 = new xiaoyu("洛洛",20,"萌妹子","福建")
//很明显,在开头多了一个类型xiaoyu,是不是更加明确清晰了。你想要的任意类型都能够自己更加精准的定位,你写ikun都行

image.png

  • 这个构造函数可以确保我们的对象是有Person的类型的(实际是constructor的属性,这个我们后续再探讨);
  • 但是构造函数就没有缺点了吗?
  • 构造函数也是有缺点的,它在于我们需要为每个对象的函数去创建一个函数对象实例

如何区分是否是构造函数

一般来说,构造函数实在跟普通函数没有区别,单纯看一个函数是看不出来的,于是社区对此有了约定俗成的规范,不是必须要遵守,但拥抱规范能够让我们平时更加方便,减少大家的理解跟沟通成本


function XiaoYu(){
//对于构造函数,我们函数名首字母会是大写。如果由多个单词组成的话,则是采用大驼峰标识
}

构造函数的缺点

  1. 为什么是f1 === f2是false,那是因为他们创建出来的对象不是同一个对象,虽然打印出来都是[Function: bar],但是确实是不一样的两个对象
  2. 那为什么不是同一个对象?
  • 第一次执行的时候,我们在foo函数里面定义了bar函数,执行foo函数的时候(函数调用),会在内存中创建一个bar的函数对象。然后我们用f1接收了这个函数对象之后,第二次执行会重新创建一个bar函数对象,然后放入f2中。此时是同时存在两个函数对象的


  1. 它在于我们需要为每个对象的函数去创建一个函数对象实例;(也就是写在开头的那句话
  • 每个对象创建一个函数实例会增加内存的开销,这可能会导致性能问题,特别是当你有大量的对象的时候。此外,如果你在构造函数中改变了对象的原型,这也会导致每个对象都有不同的原型,这可能会增加代码的复杂性


这里创建的函数对象实例为什么会是缺点?


function foo(){
    function bar(){
        console.log("你猜一不一样");
    }
    return bar
}
var f1 = foo()
var f2 = foo()
console.log(f1 === f2);//false
//我们还可以拿刚刚的案例来试一下
function xiaoyu(name,age,sex,address){
    this.name = name
    this.age = age
    this.sex = sex
    this.address = address
    //我们这里的创建对象,相当于在对象里再重复的创建对象其实是没有必要的,就是每个对象的函数去创建一个函数对象实例;为什么这个会是缺点看上面第三点
    this.eating = function(){
        console.log(this.name + "在吃鱿鱼须");
    }
    this.runding = function(){
        console.log(this.name + "在跟坤坤打篮球");
    }
}
var f1 = new xiaoyu("小余同学",20,"男","福建")
var f2 = new xiaoyu("小余同学",20,"男","福建")
//name其实都是"小余同学",他们的值是没有区别的,造成他们不相等的原因是:每个对象都有不同的原型,所以不相等
console.log(f1.eating === f2.eating);//false
console.log(f1.runding === f2.runding);//false

认识对象的原型

当我们对一个东西定义在[[]]里面的时候,证明这个是ECMA标准把它叫做这个名字,prototype翻译过来就是原型的意思

  • JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。
  • 那么这个对象有什么用呢?
  • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
  • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;
  • 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
  • 那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
  • 答案是有的,只要是对象都会有这样的一个内置属性;
  • 获取的方式有两种:
  • 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
  • 方式二:通过 Object.getPrototypeOf 方法可以获取到;

对象的原型

//我们每个对象中都有一个[[prototype]],这个属性可以称之为对象的原型(隐式原型),这里很重要
//称呼为隐式原型的原因:1.平时看不到2.以后也不会去改他3.也不会去使用他。我们只会利用他的底层原理来使用他
//解释原型的概念和看原型
var obj = {name:"小余"}
console.log(obj);//在node打印是看不到隐藏起来的[[prototype]]的,这个需要到浏览器的控制台去看一下(也就是功能是浏览器提供的)
console.log(obj.__proto__);//[Object: null prototype] {}

image.png

这个Prototype是浏览器提供的,但有些浏览器可能是没有实现这个东西,所以我们在开发的时候尽量不使用这个。但是理解内部的原理还是很有必要的


但是他提供了一个属性叫做__proto__,通过上面的图片倒数第三个我们也能够看到,在Prototype里面,那proto翻译过来其实也就是原型的意思

//通常情况下我们是不能使用__proto__这个原型的,如果你真的想要获取这个对象的话,有提供另一个办法供你获取
var obj = {name:"小余"}
//获取obj的原型,是ES5之后提供的方法,不是浏览器提供的
console.log(Object.getPrototypeOf(obj));//node打印出来的结果:[Object: null prototype] {}

原型有什么用?

//当我们从一个对象中获取某一个属性时,它会触发[[get]]操作
var obj = {name:"小余"}
console.log(obj.age)//像这样取值的时候就会触发get操作,操作分2步
//1.在当前对象中去查找对应的属性,如果找到就直接使用
//2.如果没有找到,会沿着原型去查找,也就是var obj = {name:"小余",__proto__},先从前面开始找,没找到再从__proto__(原型)找
//我们在上面是找不到obj的age属性的,因为我们都没有定义age这个属性,age除了obj.age = 18这种方式之外,还可以直接赋值在原型中,也能够找到,这就是顺着原型去查找的
var obj = {name:"小余"}
obj.__proto__.age = 18
console.log(obj.age)//18
//为什么要这么麻烦,要放到原型中,不直接放到对象里面呢?这是为了方便我们后续实现继承

函数的原型 prototype

  • 那么我们知道上面的东西对于我们的构造函数创建对象来说有什么用呢?
  • 它的意义是非常重大的,接下来我们继续来探讨;
  • 这里我们又要引入一个新的概念:所有的函数都有一个prototype的属性:
  • 这个prototype属性被称为显示原型属性
function foo(){
    
}
////函数作为对象来说,它也是有[[prototype]]隐式原型的
console.log(foo.__proto__)//{}
//函数因为它是一个函数,所以还会多出来一个显示原型属性,叫做prototype
console.log(foo.prototype)//{},这个属性是没有兼容性问题的,ECMA一开始就定义了这个属性
  • 你可能会问题,是不是因为函数是一个对象,所以它有prototype的属性呢?
  • 不是的,因为它是一个函数,才有了这个特殊的属性;
  • 而不是它是一个对象,所以有这个特殊的属性;
var obj = {}
console.log(obj.prototype)//obj就没有这个属性,undefined

再看new操作符

  • 我们前面讲过new关键字的步骤如下:
  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
//代码的内部实现如下
function foo(){
    var moni = {}
    this = {}
    this.__proto__ = foo.prototype//隐式原型指向显示原型
}


function foo(){
}
var f1 = new foo()
var f2 = new foo()
//这就是构造函数的f1跟f2的隐式原型会指向函数的显示原型
console.log(f1.__proto__ === foo.prototype);//true
console.log(f2.__proto__ === foo.prototype);//true


  • 那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype:


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

目录
相关文章
|
3月前
|
JavaScript 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
3月前
|
JavaScript 前端开发
JavaScript中的原型 保姆级文章一文搞懂
本文详细解析了JavaScript中的原型概念,从构造函数、原型对象、`__proto__`属性、`constructor`属性到原型链,层层递进地解释了JavaScript如何通过原型实现继承机制。适合初学者深入理解JS面向对象编程的核心原理。
46 1
JavaScript中的原型 保姆级文章一文搞懂
|
3月前
|
JavaScript 前端开发
Javascript如何实现继承?
【10月更文挑战第24天】JavaScript 中实现继承的方式有很多种,每种方式都有其优缺点和适用场景。在实际开发中,我们需要根据具体的需求和情况选择合适的继承方式,以实现代码的复用和扩展。
|
3月前
|
JavaScript 前端开发
如何使用原型链继承实现 JavaScript 继承?
【10月更文挑战第22天】使用原型链继承可以实现JavaScript中的继承关系,但需要注意其共享性、查找效率以及参数传递等问题,根据具体的应用场景合理地选择和使用继承方式,以满足代码的复用性和可维护性要求。
|
3月前
|
JavaScript 前端开发 开发者
js实现继承怎么实现
【10月更文挑战第26天】每种方式都有其优缺点和适用场景,开发者可以根据具体的需求和项目情况选择合适的继承方式来实现代码的复用和扩展。
37 1
|
5月前
|
自然语言处理 JavaScript 前端开发
一文梳理JavaScript中常见的七大继承方案
该文章系统地概述了JavaScript中七种常见的继承模式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承等,并探讨了每种模式的实现方式及其优缺点。
一文梳理JavaScript中常见的七大继承方案
|
5月前
|
JavaScript 前端开发
js之class继承|27
js之class继承|27
|
5月前
|
JSON JavaScript 前端开发
js原型继承|26
js原型继承|26
|
5月前
|
JavaScript 前端开发 开发者
JavaScript 类继承
JavaScript 类继承
29 1
|
5月前
|
JavaScript 前端开发
JavaScript prototype(原型对象)
JavaScript prototype(原型对象)
53 0