b
1. 认识面向对象
1.1. 面向对象是现实的抽象方式
对象是JavaScript中一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物
- 比如描述一辆车:Car,具有颜色(color)、速度(speed)、品牌(brand)、价格(price),行驶(travel)
- 比如描述一个人:Person,具有姓名(name)、年龄(age)、身高(height),吃东西(eat)、跑步(run)
用对象来描述事物,更有利于将现实的事物,抽离成代码中某个数据结构
- 所以有一些编程语言就是纯面向对象的编程语言,比Java
- 在实现任何现实抽象时都需要先创建一个类,根据类再去创建对象
1.2. JavaScript的面向对象
JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:
JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型
如果值是一个函数,那么可以称之为是对象的方法
如何创建一个对象
早期使用创建对象的方式最多的是使用Object类,并且使用new关键字来创建一个对象
- 这是因为早期很多JavaScript开发者是从Java过来的,它们也更习惯于Java中通过new的方式创建一个对象
后来很多开发者为了方便起见,都是直接通过字面量的形式来创建对象
- 这种形式看起来更加的简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来
2. 创建对象的两种方式
2.1. new Object()
1 | var obj = new Object(); |
2.2. var obj = {}
1 | var obj = { |
3. 操作对象
1 | var obj = { |
3.1. 获取属性
1 | console.log(obj.name); |
3.2. 修改属性值
1 | obj.name = "uname"; |
3.3. 删除属性
1 | delete obj.age; |
4. 对属性操作的控制
当属性直接定义在对象内部时,就不能对这个属性进行一些限制
- 比如这个属性是否是可以通过delete删除的
- 这个属性是否能在for-in遍历的时候被遍历出来
- 等等
如果想要对一个属性进行比较精准的操作控制,就可以使用
属性描述符
- 通过属性描述符可以精准的添加或修改对象的属性
- 属性描述符需要使用
Object.defineProperty
来对属性进行添加或者修改
4.1. defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
1
Object.defineProperty(obj, prop, descriptor);
可接收三个参数
- obj:要定义属性的对象
- prop:要定义或修改的属性的名称或 Symbol
- descriptor:要定义或修改的
属性描述符
返回值
- 被传递给函数的对象
4.2. 属性描述符分类
- 属性描述符的类型有两种
- 数据属性(Data Properties)描述符(Descriptor)
- 存取属性(Accessor访问器 Properties)描述符(Descriptor)
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
4.2.1. 数据属性描述符
数据数据描述符有如下四个特性
Configurable
:true/false属性是否可以通过delete删除属性
使用 Object.defineProperty() 添加的属性值是否可以修改
直接在一个对象上定义某个属性时,这个属性的Configurable为true
通过属性描述符定义一个属性时,这个属性的Configurable默认为false
Enumerable
:true/false属性是否可以通过for-in或者Object.keys()返回该属性
直接在一个对象上定义某个属性时,这个属性的Enumerable为true
通过属性描述符定义一个属性时,这个属性的Enumerable默认为false
Writable
:表示是否可以修改属性的值- 直接在一个对象上定义某个属性时,这个属性的Writable为true
- 通过属性描述符定义一个属性时,这个属性的Writable默认为false
value
:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改- 默认情况下这个值是undefined
4.2.2. 数据属性描述符测试代码
4.2.2.1. configurable
1 | var obj = { |
4.2.2.2. enumable
1 | var obj = { |
4.2.2.3. writable
1 | var obj = { |
4.2.3. 存取属性描述符
数据数据描述符有如下四个特性
Configurable
:true/false属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符
和数据属性描述符是一致的
直接在一个对象上定义某个属性时,这个属性的Configurable为true
通过属性描述符定义一个属性时,这个属性的Configurable默认为false
Enumerable
:true/false属性是否可以通过for-in或者Object.keys()返回该属性
和数据属性描述符是一致的
直接在一个对象上定义某个属性时,这个属性的Enumerable为true
通过属性描述符定义一个属性时,这个属性的Enumerable默认为false
get
:获取属性时会执行的函数- 默认为undefined
set
:设置属性时会执行的函数- 默认为undefined
4.2.4. 存储属性描述符测试代码
1 | var obj = { |
4.3. defineProperties
Object.defineProperties()
方法直接在一个对象上定义多个新的属性或修改现有属性,并且返回该对象
1 | var obj = { |
等价于
1 | var obj = { |
4.4. getOwnPropertyDescriptor
获取对象中某一个属性的属性描述符
1 | var obj = { |
4.5. getOwnPropertyDescriptors
获取对象中所有属性的属性描述符
1 | var obj = { |
4.6. preventExtensions
- 禁止对象扩展新属性,给一个对象添加新的属性会失败(在严格模式下会报错)
1 | var obj = { |
4.7. seal
密封对象,不允许配置和删除属性
- 实际是调用preventExtensions
- 并且将现有属性的
configurable:false
1 | var obj = { |
4.8. freeze
冻结对象,不允许修改现有属性
- 实际上是调用seal
- 并且将现有属性的writable:false
1 | var obj = { |
5. 创建多个对象的方案
希望如果创建一系列的对象:比如Person对象
- 包括张三、李四、王五、李雷等等,他们的信息各不相同
- 那么采用什么方式来创建比较好
目前学习了两种方式
- new Object方式
- 字面量创建的方式
这些方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码
5.1. 工厂模式
- 工厂模式其实是一种常见的设计模式
- 通常会有一个工厂方法,通过该工厂方法可以产生想要的对象
- 工厂方法创建对象有一个比较大的问题
- 在打印对象时,对象的类型
都是Object类型
- 但是从某些角度来说,这些对象应该有一个他们共同的类型
- 在打印对象时,对象的类型
1 | function person(name, age, height, address){ |
5.2. 构造函数
5.2.1. 认识构造函数
什么是构造函数
- 构造函数也称之为构造器(
constructor
),通常是在创建对象时会调用的函数 - 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法
- 但是JavaScript中的构造函数有点不太一样
- 构造函数也称之为构造器(
JavaScript中的构造函数是怎么样的
- 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别
- 那么如果这么一个普通的函数被使用
new操作符
来调用了,那么这个函数就称之为是一个构造函数
5.2.2. new操作符调用的作用
- 如果一个函数被使用new操作符调用了,那么它会执行如下操作
- 在内存中创建一个新的对象(空对象)
- 这个对象内部的
prototype
属性会被赋值为该构造函数的prototype属性 - 构造函数内部的this,会指向创建出来的新对象
- 执行函数的内部代码(函数体代码)
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
1 | function Person(){ |
5.2.3. 通过构造函数创建多个对象
- 这个构造函数可以确保对象是有Person的类型的(实际是constructor的属性)
- 但是构造函数也是有缺点的,它需要为每个对象的函数去创建一个函数对象实例
1 | function Person(name, age, height, address){ |
5.3. 构造函数和原型组合
构造函数的方式创建对象时,有一个弊端
会创建出重复的函数,比如running、eating这些函数
那么有没有办法让所有的对象去共享这些函数呢?
可以,将这些函数放到Person.prototype的对象上即可
1 | function Person(name, age, height, address){ |
6. 对象的原型
6.1. 认识对象的原型
JavaScript当中每个对象都有一个特殊的内置属性prototype,这个特殊的对象可以指向另外一个对象
那么这个对象有什么用呢?
- 通过引用对象的属性key来获取一个value时,它会触发 Get 的操作
- 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它
- 如果对象中没有改属性,那么会访问对象prototype内置属性指向的对象上的属性
1
2
3var obj = {};
obj.__proto__.age = 18;
console.log(obj.age);那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
- 答案是有的,只要是对象都会有这样的一个内置属性
获取的方式有两种
方式一:通过对象的
__proto__
属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题)方式二:通过
Object.getPrototypeOf
方法可以获取到
1 | var obj = {} |
6.2. 函数的原型
引入一个新的概念:
所有的函数都有一个prototype的属性
- 属性中有 constructor属性 指向构造函数本身
1
2
3
4
5
6function foo(){}
// {}
console.log(foo.prototype);
// [Function: foo]
console.log(foo.prototype.constructor);是不是因为函数是一个对象,所以它有prototype的属性
- 不是的
- 因为它是一个
函数
,才有了prototype属性
;而它作为一个对象
,有prototype隐式原型
- 单作为对象并没有prototype属性
1
2
3var obj = {};
// undefined
console.log(obj.prototype);
6.3. new操作符
new关键字的步骤如下
- 在内存中创建一个新的对象(空对象)
- 这个对象内部的 prototype 属性会被赋值为该构造函数的 prototype 属性
那么也就意味着通过Person构造函数创建出来的所有对象的prototype属性都指向Person.prototype
1 | function Person(){ } |
6.4. 创建对象的内存表现
6.5. constructor属性
- 事实上原型对象上面是有一个属性的:constructor
- 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象
1 | function Person(){} |
6.6. 重写原型对象
如果需要在原型上添加过多的属性,通常会重新整个原型对象
1
2
3
4
5
6
7
8
9
10function Person(){
}
Person.prototype = {
name: 'why',
age: 18
}
var p = new Person();
// why
console.log(p.name);每创建一个函数, 就会同时创建它的prototype对象,这个对象也会自动获取constructor属性
- 相当于给prototype重新赋值了一个对象,那么这个
新对象的constructor属性, 会指向Object构造函数
,而不是Person构造函数了
1
2
3
4
5
6
7
8
9
10
11
12/**
{
name: {
value: 'why',
writable: true,
enumerable: true,
configurable: true
},
age: { value: 18, writable: true, enumerable: true, configurable: true }
}
*/
console.log(Object.getOwnPropertyDescriptors(Person.prototype));- 相当于给prototype重新赋值了一个对象,那么这个
6.7. 原型对象的constructor
如果希望constructor指向Person,那么可以手动添加
1
2
3
4
5
6
7
8Person.prototype = {
constructor: Person,
name: 'test',
age: 18,
eating: function(){
console.log(this.name + " eating");
}
}上面的方式虽然可以, 但是也会造成constructor的 Enumerable特性被设置了true.
- 默认情况下, 原生的constructor属性是不可枚举的
- 如果希望解决这个问题, 就可以使用Object.defineProperty()函数了
1
2
3
4Object.defineProperty(Person.prototype,"constructor",{
enumerable: false,
value: Person
});
7. 继承
7.1. JavaScript中的类和对象
1 | function Person(){ |
- 编写上述代码时,如何称呼这个Person呢?
- 在JS中Person应该被称之为是一个构造函数
- 从很多面向对象语言过来的开发者,也习惯称之为类,因为类可以创建出来对象p1、p2
- 如果从面向对象的编程范式角度来看,Person确实是可以称之为类的
7.2. 面向对象的特性 – 继承
面向对象有三大特性:
封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中)
- 多态:不同的对象在执行时表现出不同的形态;
那么继承是做什么呢?
- 继承可以将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可
那么JavaScript当中如何实现继承呢?
- 利用JavaScript原型链机制实现继承
7.3. JavaScript原型链
- 从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取
1 | var obj = { |
7.3.1. Object的原型
那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?
1
2// [Object: null prototype] {}
console.log(obj.__proto__.__proto__.__proto__.__proto__);它打印的是 [Object: null prototype] {}
- 事实上这个原型就是
最顶层的原型
- 从Object直接创建出来的对象的原型都是 [Object: null prototype] {}。
- 事实上这个原型就是
[Object: null prototype] {} 原型有什么特殊吗
- 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型
- 特殊二:该对象上有很多默认的属性和方法
7.3.2. 创建Object对象的内存图
1 | var obj = { |
7.3.3. 原型链关系的内存图
1 | var obj = { |
7.3.4. Object是所有类的父类
从Object原型可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象
1 | function Person(name,age){ |
7.4. 通过原型链实现继承
- 如果现在需要实现继承,那么就可以利用原型链来实现了
- 目前stu的原型是p对象,而p对象的原型是Person默认的原型,里面包含running等函数
- 注意:步骤4和步骤5不可以调整顺序,否则会有问题
7.4.1. 代码
1 | // 1. 定义父类构造函数 |
7.4.2. 原型链继承的弊端
- 某些属性其实是保存在p对象上的
- 通过直接打印对象是看不到这个属性的
- 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
- 不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化)
7.5. 借用构造函数继承
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术
- constructor stealing(借用构造函数或者称之为经典继承或者称之为伪造对象)
- steal是偷窃、剽窃的意思,但是这里可以翻译成借用
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数
因为函数可以在任意的时刻被调用
因此通过apply()和call()方法也可以在新创建的对象上执行构造函数
7.5.1. 代码
1 | // 1. 定义父类构造函数 |
7.5.2. 组合借用继承的问题
组合继承是JavaScript最常用的继承模式之一
- 点到为止, 组合来实现继承只能说问题不大
- 但是它依然不是很完美,但是基本已经没有问题了
- 不成问题的问题, 基本一词基本可用, 但基本不用
组合继承存在什么问题呢
- 组合继承最大的问题就是无论在什么情况下,都会
调用两次父类构造函数
- 一次在创建子类原型的时候
- 另一次在子类构造函数内部(也就是每次创建子类实例的时候)
- 组合继承最大的问题就是无论在什么情况下,都会
另外,如果按照流程走了上面的每一个步骤,会发现
所有的子类实例事实上会拥有两份父类的属性
一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是
person.__proto__
里面)当然,这两份属性无需担心访问出现问题,因为默认一定是访问实例本身这一部分的
7.6. 原型式继承函数
原型式继承的渊源
- 这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的一篇文章说起: Prototypal Inheritance in JavaScript(在JS中使用原型式继承)
- 在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的
- 为了理解这种方式,先再次回顾一下JavaScript想实现继承的目的:重复利用另外一个对象的属性和方法
最终的目的:student对象的原型指向了person对象
1 | var obj = { |
这就是create方法的内部实现
1 | var obj = { |
7.7. 寄生式继承函数
- 寄生式(Parasitic)继承
- 寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的
- 寄生式继承的思路是
结合原型类继承和工厂模式
的一种方式 - 即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回
1 | function createObject(o){ |
7.8. 寄生组合式继承
组合继承是比较理想的继承方式, 但是存在两个问题
- 问题一:构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候
- 问题二:父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中
事实上, 可以利用寄生式继承将这两个问题给解决掉
- 需要先明确一点:在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容不再被需要
- 这个时候, 还需要获取到一份父类型的原型对象中的属性和方法
能不能直接让子类型的原型对象 = 父类型的原型对象呢
- 不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改
- 使用前面的寄生式思想就可以了
7.9. 原型继承关系
7.10. ES6类的继承
- 在ES5中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的
- 在ES6中新增了使用extends关键字,可以方便的帮助实现继承
7.10.1. extends
7.10.2. super关键字
- super关键字有不同的使用方式
- 注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数
- super的使用位置有三个:子类的构造函数、实例方法、静态方法
7.11. 继承内置类
- 有些类继承自内置类,比如Array
7.12. 类的混入mixin
- JavaScript的类只支持单继承:也就是只能有一个父类
- 那么在开发中需要在一个类中添加更多相似的功能时,应该如何来做呢?
- 这个时候可以使用混入(mixin)
8. 对象的方法补充
hasOwnProperty
- 对象是否有某一个属于自己的属性(不是在原型上的属性)
in/for in 操作符
- 判断某个属性是否在某个对象或者对象的原型上
instanceof
- 用于检测构造函数的pototype,是否出现在某个实例对象的原型链上
isPrototypeOf
- 用于检测某个对象,是否出现在某个实例对象的原型链上
9. 认识class定义类
按照前面的构造函数形式创建 类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解
- 在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类
- 但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已
- 所以学好了前面的构造函数、原型链更有利于理解类的概念和继承关系
那么,如何使用class来定义一个类呢
- 可以使用两种方式来声明类:类声明和类表达式
9.1. 类和构造函数的异同
- 类的一些特性
- 它和构造函数的特性其实是一致的
9.2. 类的构造函数
如果希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢
每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor
通过new操作符,操作一个类的时候会调用这个类的构造函数constructor
每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常
当通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作
- 在内存中创建一个新的对象(空对象)
- 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性
- 构造函数内部的this,会指向创建出来的新对象
- 执行构造函数的内部代码(函数体代码)
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
9.3. 类的实例方法
- 定义的属性都直接放到了this上,也就意味着它是放到了创建出来的新对象中
- 对于实例的方法,希望放到原型上的,这样可以被多个实例来共享
- 这个时候可以直接在类中定义
9.4. 类的访问器方法
- 对象的属性描述符时有讲过对象可以添加setter和getter函数的,那么类也是可以的
9.5. 类的静态方法
- 静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义
Gitalking ...