08_JS面向对象
深入JS面向对象
面向对象初识
创建对象的方式
// 方式一:通过new Object()创建 |
对属性的操作
var obj = { |
这种直接定义在对象内部或者直接添加到对象内部的属性,我们不能对其做出限制:比如这个属性是否可以通过delete删除,是否可以在for-in遍历的时候被遍历出来
Object.defineProperty()
如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符
通过属性描述符可以精准的添加或修改对象的属性
属性描述符需要使用 Object.defineProperty 来对属性进行添加或修改
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
接收三个参数
- obj:要定义属性的对象
- prop: 要定义或修改的属性的名称或Symbol
- descriptor:要定义或修改的属性描述符
第三个参数的属性描述符分为两类:
- 数据属性描述符
configurable,enumerable,writable,value
// 直接在一个对象上定义某个属性时,描述符的默认值为
// value: 赋值的value
// configurable: true
// enumerable: true
// writable: true
var obj = {
name: 'jackson',
age: 18
}
Object.defineProperty(obj, "height", {
value: 1.88, // 属性值(默认为 undefined)
enumerable: true, // 对应属性是否可以枚举(默认为false)
writable: true, // 是否可以修改属性值(默认为false)
configurable: true, // 属性能否被删除(默认为false),属性的描述符能否被改变
})
// 枚举
for(var key in obj) {
console.log(key); // name age height 可枚举
}
console.log(Object.keys(obj)); // [ 'name', 'age', 'height' ] 可枚举
// 修改属性值
obj.height = 2
console.log(obj.height); // 2 可修改
// 删除属性
delete obj.height
console.log(obj.height); // undefined 删除成功存取属性描述符
enumerable,configurable,get,set不用value,writable,而是用 get,set
var obj = {
name: 'jackson',
age: 18,
_address: "广东"
}
// 私有属性,js里面是没有严格意义的私有属性的,所以我们实际上也能直接访问,但是在社区人们约定 下划线_开头的属性定义为私有属性
// 1.隐藏某一个私有属性(address)不希望直接被外界使用和赋值
// 2.如果我们希望截获某个属性被访问和设置值的过程时,也会使用存储属性描述符
Object.defineProperty(obj, "address", {
enumerable: true, // 可枚举
configurable: true, // 可删除,可修改描述符
get: function() {
console.log("获取了一次address的值")
return this._address
},
set: function(value) {
console.log("设置了address的值")
this._address = value
}
})
console.log(obj.address); // 广东- 数据属性描述符
可枚举属性的补充
var obj = { |
Object.defineProperties()
Object.defineProperties() 方法可以直接在一个对象上定义多个新的属性
var obj = { |
对象方法补充(了解)
- 获取对象的属性描述符
- getOwnPropertyDescriptor
- getOwnPropertyDescriptors
- 禁止对象扩展新属性
- preventExtensions:给一个对象添加新的属性会失败(在严格模式下会报错)
- 密封对象,不允许配置和删除属性:seal
- 实际是调用preventExtensions
- 并且将现有属性的configurable:false
- 冻结对象,不允许修改现有属性:freeze
- 实际上是调用seal
- 并且将现有属性的writable: false
创建多个对象的方式
通过想要创建多个对象的目的,来引出后面的构造函数
前面我们通过 new Object,字面量的方式创建对象,但是这两种方式有一个很大的弊端:
创建同样的对象时,需要编写重复代码
比如说字面量:
var p1 = {name: , age: } |
创建对象的方式 - 工厂模式
- 工厂模式其实是一种常见的设计模式
- 通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象
- 工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型
- 但是从某些角度来说,这些对象应该有一个他们共同的类型
function createPerson(name, age, height) { |
认识构造函数
什么是构造函数?
- 构造函数也称之为构造器(constructor),通常我们在创建对象时会调用的函数
- JavaScript 中,如果一个普通的函数被使用 new 操作符来调用了,那么这个函数就称之为是一个构造函数
所以说构造函数也是一个普通函数,只不过用 new 去调用,就称为构造函数
new也是可以调用函数的喔
new操作符
如果一个函数被使用new操作符调用了,那么它会执行如下操作:
- 在内存中创建一个新的对象(空对象)
- 这个对象内部的 **[[prototype]]**属性会被赋值为该构造函数的 prototype属性
- 构造函数的 this ,会指向创建出来的新对象
- 执行函数的内部代码
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
构造函数创建对象
// 规范:构造函数的首字母一般是大写 |
- 这个构造函数可以确保我们的对象是有Person的类型的
- 但是也有缺点:我们需要给每个对象的函数去创建一个函数对象实例(开辟新的内存空间)
对象的原型 [[prototype]]
JavaScript 中,每个对象都有一个特殊的内置属性 [[prototype]],这个属性称之为对象的原型(隐式原型)(只要是对象,就会有这个内置属性)
[[prototype]] 指向一个对象(也就是说它也是一个对象)
那么这个对象有什么用呢?
- 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作
- 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它
- 如果对象中没有该属性, 那么会沿着它的原型去查找 [[prototype]]
如果通过字面量直接创建一个对象,那么这个对象也有[[prototype]]这个属性
如何查看这个属性呢
var obj = { name: "jackson"}
var info = {}
// 创建出来的对象上都有 [[prototype]]属性
// 如何查看这个属性?
// 早期的ECMA是没有规范如何去查看 [[prototype]]
// 给对象中提供了一个属性 __proto__, 可以让我们查看一下这个原型对象(浏览器提供)
console.log(obj.__proto__);// [Object: null prototype] {}
// 相当于(伪代码)
var obj = {name: 'jackson', __proto__: {}}
// ES5之后提供的Object.getPrototypeOf()查看
console.log(Object.getPrototypeOf(obj)); // {}
// 例如找 age 这个属性,该对象本身没有,沿着原型查找
obj.__proto__.age = 18
console.log(obj.age); // 18
函数的原型 prototype
- 所有的函数都有一个 prototype 属性(显式原型)
function Foo() { |
再看回new操作符其中的一个步骤:
这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
也就意味着我们通过 Foo 构造函数创建出的所有对象的 [[prototype]] 都指向 Foo.prototype
var f1 = new Foo() |
创建对象的内存表现
可以看到:
- 构造函数的 prototype 属性指向该函数的原型对象
- 原型对象身上有一个 constructor 属性指回构造函数本身
- new出来的实例对象 p1,p2 对象身上有
__proto__
属性也指向构造函数的原型对象
函数原型上的属性constructor
- 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象
function Foo() {} |
- 我们也可以在prototype上添加自己的属性
Foo.prototype.name = "jackson" |
- 直接修改整个prototype对象(赋值,新开内存空间)
Foo.prototype = { |
原来的foo函数的原型对象因为没有对它的引用,会被回收的
创建对象的方式 — 构造函数和原型组合
function Person(name, age, address) { |
JavaScript中的类和对象
function Person() {} |
在 JS 中,Person应该被称之为是一个构造函数
但是从很多面向对象语言的开发者习惯称之为类,因为类可以帮我们创建出来实例对象,也是可以的
面向对象的特性 - 继承
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中)
- 多态:不同的对象在执行时表现出不同的形态
JavaScript原型链
原型链的理解
var obj = { |
这样一直向上找的话,那么顶层原型究竟是什么呢?
var obj = {name: "Jackson"} |
顶层对象有什么特别吗?该对象上有很多默认的属性和方法
顶层原型又来自哪里呢?下面我们创建Object对象看看
// 创建一个对象(这种方式相当于下面一种方式的语法糖,本质都是创建一个对象) |
因此,原型链最顶层的原型对象就是Object的原型对象
也就是说,Object是所有类的父类
function Person(name) { |
通过原型链实现继承
为什么需要有继承?
如果没有继承,我们想创建多个类,类里面的属性和方法很多是一样的,那么我们就会写很多重复的代码,所以主要是为了代码的复用
// Student |
原型链的继承方案
自己画画图更好理解
// 父类:公共属性和方法 |
借用构造函数实现继承
使用call调用构造函数,
// 父类:公共属性和方法 |
但是这种方法依然存在弊端:
- Person 至少被调用两次(一开始new Person一次,后面Person.call又会调用Person)
- stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要(new Person的时候的)
注意:
那么我们换一种获得父类方法的方法,直接将父类原型赋值给子类?
肯定是不行的,因为以后给某个子类添加方法的时候,会使所有的子类都有该方法,显然是不行的
因为所有子类的prototype都指向同一个父类的原型
原型式继承函数 - 对象
我们先实现对象的继承,后面再扩展到类
var obj = { |
Object.create() :https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
寄生式继承函数(了解)
寄生式继承的思路是结合原型类继承和工厂模式的一种方式
即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回
var personObj = { |
寄生组合式继承(最终方案)
利用寄生式继承将组合式继承的两个问题解决
- 首先我们需要明确,当我们在子类的构造函数中调用父类.call(this,参数)的时候,就会将父类的属性和方法复制一份到子类中,所以父类本身里面的内容我们是不需要的
- 然后,我们还需要获取到一份父类的原型对象中的属性和方法
// 如果不想使用Object.create这个方法的话,我们可以定义前面说过的方法 |
JS原型的补充
hasOwnProperty
- 判断对象是否有某个属于自己的属性,不包括在原型上的
in/ for in 操作符
- 判断某个属性是否在某个对象或者对象的原型上
var obj = { |
instanceof
用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
function Person() {} |
isPrototypeOf(了解)
用于检测某个对象,是否出现在某个实例对象的原型链上
var info = Object.create(obj) |
对象-函数-原型之间的关系
var obj = { |
最后 上图!