对象、类与面向对象编程
理解对象
创建自定义对象的两种方法
- 创建自定义对象的通常方式是创建Object的一个新实例,再给这个新实例添加方法和属性
- 使用对象字面量
属性的类型
-
对象属性分为两种
-
数据属性
-
数据属性有4个特性描述它们的行为
- [[Configurable]],表示属性是否可以通过delete删除,是否可以修改特性,是否可以把它改为访问器属性,默认值为true。
- [[Enumerable]],表示属性是否可以通过for-in循环返回,默认值为true。
- [[Writable]],表示属性的值是否可以修改,默认值为true。
- [[value]],这里存放属性的值,默认值为undefined。
-
修改属性的默认特性
-
只有使用Object.definedProperty()可以修改特性的默认值。
- Object.definePorperty()接受三个参数,1.属性从属的对象,2.属性的名称,3.一个描述符对象。
-
-
-
访问器属性(用来获取不想在对象外部被访问的数据属性)
-
访问器属性有4个特性描述它们的行为
- [[Configurable]],表示属性是否可以使用delete删除,是否可以修改特性,是否可以把它改为访问器属性,默认值为true。
- [[Enumerable]],表示属性是否可以通过for-in循环返回,默认值为true。
- [[Get]],获取函数,在读取不想被外部访问的属性时调用,默认值为undefined。
- [[Set]],设置函数,在写入不想被外部访问的属性时调用,默认值为undefined。
-
访问器属性的定义必须使用Object.definedProperty()。
-
获取函数和设置函数这两个特性不一定都要定义。只定义获取函数表示属性是只读的。
-
-
定义多个属性
- 使用Object.defineProperties()可以同时定义多个属性并设置它们的特性。
合并对象
-
合并对象是指把源对象所有的本地属性一起复制到目标对象上。
-
使用Object.assign()可以合并对象。这个方法接收一个目标对象和一个或多个源对象作为参数。
- Object.assign()对源对象属性执行的是浅复制。
-
对象标识及相等判定
- Object.is()可以解决NaN之间的相等判定,Object.is(NaN,NaN)返回true。
增强的对象语法(ES6为定义和操作对象新增了很多有用的语法糖特性)
-
简写属性名语法
- 简写属性名只需要使用变量名,属性名就会被自动解释为和变量名相同。
-
可计算属性(可计算属性使得可以动态命名属性)
-
简写方法名
-
在给对象定义方法时,通常要写一个方法名、冒号,然后再引用一个匿名函数表达式。
- 间歇方法名可以直接在方法名后面跟函数体。
-
对象解构(ES6新增)(null和undefined不能被解构)
-
对象解构可以在一条语句中使用嵌套数据实现一个或多个赋值操作。(含义是解构源对象对变量进行赋值)
-
使用解构可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。(如果想让变量直接使用属性的名称,那么可以使用简写语法)
-
如果解构时引用的属性不存在,那么变量的值就是undefined。
- 也可以在解构赋值的时候定义默认值,可以解决引用属性不存在导致undefined的问题。
-
-
-
嵌套解构
- 解构赋值可以使用嵌套结构以匹配嵌套的属性(在外层属性没有定义的时候不能使用嵌套解构)
-
部分解构
- 如果开始的解构赋值成功而之后的解构赋值失败,那么只会完成部分解构。
使用ES5来模拟类似类的行为
创建对象
-
创建对象的三种方式
- 对象字面量。
- 使用new关键字创建对象
- Object.create()创建对象(ES5特性)。
-
使用Object构造函数或对象字面量可以很方便的创建对象,但是当需要创建多个具有相同属性的对象需要重复编写很多代码
-
工厂模式
-
解决了创建多个类似对象的问题
- 但是工厂模式不能确定创建的对象实例的类型(只支持person1 instanceof Object)。
-
-
构造函数模式(构造函数的函数名首字母要大写)
-
使用构造函数创建对象实例应该使用new关键字,这样调用构造函数会执行以下5步操作。
- 1.在内存中创建一个新的空对象。
- 2.这个新对象的[[Prototype]]特性被赋值为构造函数的prototype属性。
- 3.构造函数内部的this指向这个新对象。
- 4.执行构造函数内部的代码。
- 5.返回这个对象。
-
构造函数模式创建的对象实例可以确定类型(支持 person1 instanceof Person 和 person1 instanceof Object) 。
- person1 instanceof Object等于true,因为所有的自定义对象都继承自Object。
-
在实例化时,如果不想传入参数,构造函数后面的括号可加可不加,只要有new操作符就可以调用构造函数。
-
任何函数只要使用new操作符调用就是构造函数。不使用new操作符的就是普通函数。
- 如果不使用new调用了构造函数,那么属性和方法会被添加至window对象。(因为this指向Global对象,在浏览器中就是window对象)。
-
构造函数的问题
-
构造函数的问题是构造函数内部的方法会在每个实例中都创建一遍,因为ECMAScript中函数是对象,所以每个构造函数创建的实例都需要创建一个Function实例。
-
把函数的定义放在构造函数的外部可以解决这个问题。这样对象的方法将被定义在全局作用域,而构造函数内部只是一个指向外部函数的指针。(相当于对象的所有实例共享了全局作用域上的函数)。
- 虽然这样解决了相同逻辑的函数重复创建的问题,但会带来需要在全局作用域定义多个函数,导致自定义类型引用的代码不能很好的聚集在一起的问题。(这个问题可以通过原型模式解决)。
-
-
-
-
原型模式
-
理解原型
-
每个函数都会创建一个prototype属性,这个属性指向一个对象(prototype属性可以拥有属性和方法)。
- prototype属性应该包含的是由特定引用类型的实例共享的属性和方法。(prototype指向的对象就是调用构造函数创建的对象实例的原型)。
-
在自定义构造函数时,原型对象只会默认获得constructor属性,其他方法都继承自Object。(原型对象的constructor属性指向这个构造函数)。
- 构造函数和它对应的原型对象循环引用(构造函数的prototype属性指向原型对象,原型对象的constructor属性指向构造函数)。
-
使用原型对象(prototype)的好处是,它上面定义的属性和方法可以被所有对象实例共享。
- 每次调用构造函数创造一个对象实例,这个实例内部的[[Prototype]]指针就会指向构造函数的原型对象。(JavaScript中不能访问[[Prototype]],但主流浏览器Firefox、Safari和Chrome会在每个实例上暴露_proto_属性,用来访问实例的构造函数的原型对象)。
-
原型对象具有__proto__属性,指向原型对象的原型对象。
-
ECMAScript的Object类型有一个方法叫 Object.getPrototypeOf(),它返回参数的[[Prototype]]的值。(所以使用Object.getPrototypeOf()可以方便的取得一个对象的原型对象)。
- 正常原型链会终止于Object的原型对象,因为Object的原型对象的原型对象是null。
- 只有函数才有prototype属性(指向原型对象),对象实例只有__proto__属性指向原型对象。(实例的__proto__属性实际上指向它的[[Prototype]]隐藏特性,[[Prototype]]特性指向构造函数的原型对象)。
-
-
原型层级
-
当我们需要访问对象实例的属性时,JavaScript首先会搜索对象实例本身,如果在对象实例本身没有发现要访问的属性,那么会通过__proto__属性进入对象实例的原型对象搜索。
-
在调用person1.sayName()时,JavaScript首先去person1中搜索是否存在sayName()方法,然后并没有在person1中发现sayName()方法,JavaScript就会去person1的原型对象中搜索。(这就是原型用于在多个对象间共享属性和方法的原理)。
- 因此对象实例也可以访问到constructor属性。
-
-
虽然可以在对象实例中访问到原型对象的属性,但是对象实例不可能修改原型对象中的属性。(如果在实例中添加了一个和原型对象中属性同名的属性,那么这个属性就会遮住原型中的属性)。
-
如果在person1中创建一个属性name,当需要访问这个属性时,JavaScript就会直接返回实例中的name属性的值,不会再去原型中寻找name属性。(实例中的属性遮住了原型中的同名属性)。
- 使用delete完全删除实例上的同名属性之后,才能再次访问到原型中的属性。(delete person1.name;)
-
-
hasOwnProperty()方法用于确定某个属性是在实例上还是原型对象上。(会在属性存在于调用它的实例上时返回true)。
- 当person1中有属性name时,person1.hasOwnPorperty(“name”)返回true。
-
-
原型和in操作符
-
有两种方式使用in操作符
-
单独使用in操作符
-
单独使用in操作符时,将会在可以通过对象访问指定属性时返回true。(不管这个属性在实例上还是原型上)。
- 要确定某个属性是否存在于原型上,可以配合hasOwnProperty()使用。(!person1.hasOwnProperty(“name”) && name in person1;如果返回true,说明name属性存在于原型上)。
-
-
在for-in循环中使用
-
在for-in循环中使用in操作符时,可以通过对象访问到且可以被枚举的属性都会返回。(可以被枚举的属性是指[[Enumerable]]特性是true的属性)。
-
要获得对象上的所有可枚举实例属性,可以使用Object.keys()方法。(这个方法接受一个对象作为参数,返回该对象所有可枚举属性名称的字符串)。不会返回原型中的属性。
- Object.getOwnPropertyNames()会返回所有实例属性,不论是否可以枚举。
-
-
-
-
-
原型的动态性
-
如果先创建了一个对象实例person1,再向原型中添加了一个属性age,使用person1.age也是可以访问到这个属性的。
- 主要原因是原型和实例之间松散的联系,在使用person1调用age属性时,会先去找person1中是否存在age属性,如果不存在则会去找原型中是否存在age属性。(因为实例和原型之间的连接是指针而不是实例保存了一个原型的副本)。
-
-
原型的问题
- 原型的主要问题在于原型中的引用值也是在所有实例间共享的。(如果原型中包含一个字符串数组属性,那么person1向这个字符串数组中添加值,到person2访问这个字符串数组时,这个字符串数组包含有person1添加的值)。
-
-
继承
-
面向对象编程语言通常支持两种继承方式
-
接口继承
-
接口继承继承函数签名。
-
因为JavaScript不存在函数签名,所以不可能实现接口继承。
- 函数签名由函数名和参数组成。如在C语言中必须先进行函数签名才能调用函数。
-
-
-
实现继承
-
实现继承继承实际的方法。
- 实现继承是JavaScript唯一支持的继承方式,主要通过原型链实现。
-
-
-
原型链
-
原型链的实现继承的关键是子对象的原型是父对象的实例。
- 原型链对属性和方法的搜索一直会持续到原型链的末端。(原型链的末端是Object的原型,因为Obejct的原型的原型是null)。
-
默认原型
- 所有函数的默认原型都是Object的实例。(这意味着所有函数的默认原型都有一个[[Prototype]]指向Object.prototype)。
-
子类覆盖父类方法
- 子类有时候需要覆盖父类的方法,或增加父类没有的方法,这些方法必须在原型赋值为父类的实例之后添加。
-
原型和实例的关系
-
原型和实例的关系可以通过2种方式确定
-
使用instanceof操作符
- 如果一个实例的原型链种出现过相应的构造函数,那么instanceof返回true。(instance instanceof Object返回true)。
-
使用isPrototypeOf()方法
- 原型链中的每个原型都可以调用isPrototypeOf(),只要原型链中包含这个原型,这个方法就返回true。(Object.isPrototypeOf(instance)返回true)。
-
-
-
原型链的问题
-
原型链的问题主要出现在原型中包含引用值的时候。(如原型中包含字符串数组)。
-
原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会在原型中定义的原因。
- 只有保持引用值是实例属性才能避免这个问题,但使用原型链继承时,原型是父类的实例,那么父类构造函数定义的实例属性就变成了原型属性。
-
-
-
-
盗用构造函数
- 盗用构造函数用于解决原型包含引用值导致的继承问题。
-
组合继承
-
将盗用构造函数和原型链组合起来,使用盗用构造函数继承父类的属性,使用原型链继承方法。
- 组合继承是Javascript使用最多的继承方式。
-
使用class关键字定义类(ES6新增)
类定义
-
定义类的2种方式
- class Person {}
- const Person = class {};
-
类受块作用域限制。(在块内声明的类不能在块外引用)。
-
类的构成
-
类可以包含5个部分
- 构造函数方法
- 实例方法
- 获取函数
- 设置函数
- 静态类方法
-
默认情况下,类定义中的代码都在严格模式下执行。
-
类构造函数
-
constructor关键字用于在类定义块内部创建类的构造函数。
-
方法名constructor会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。
- 构造函数的定义不是必需的,不定义构造函数相当于把构造函数定义为空函数。
-
-
实例化
-
使用new操作符实例化类Person的操作等于使用new调用其构造函数。(唯一的不同在于JavaScript解释器知道使用new和类意味着应该使用constructor函数进行实例化)。
-
使用new调用类的构造函数会执行5步操作。
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性。
- 构造函数内部的this被赋值为这个新对象。(this指向新对象)。
- 执行构造函数内部的代码。(给新对象添加属性)。
- 返回这个对象。
-
-
类实例化时使用的参数会用做构造函数的参数。(如果不需要参数,那么类名后面的括号可以不写)。
-
类构造函数和构造函数的区别
- 调用类构造函数必须使用new操作符,如果没有使用new操作符将会报错。
- 普通构造函数如果不使用new操作符,就会以全局的this(通常是window)作为内部对象。
-
类构造函数实例化之后将会成为普通的实例方法。(但作为类构造函数,仍然需要使用new操作符才能调用)。
- 实例可以使用new操作符调用类构造函数。(这时类构造函数对于这个实例来说是一个实例方法)。
-
-
把类当成特殊函数
-
ECMAScript中没有正式的类这个类型。(ECMAScript的类就是一种特殊的函数,使用typeof检测类标识符将返回function)。
- 类标识符有prototype属性,而这个原型也有一个constrcutor属性指向类自身。
-
可以使用instanceof操作符检查一个对象与类构造函数,以确定这个对象不是类的实例。(只不过这时的类构造函数要使用类标识符)。
- animal instanceof Animal返回true。
-
类可以像函数一样定义在任何地方,如数组中。
-
实例、原型和类成员
-
类的语法可以非常方便的定义应该存在于实例上的成员、应该存在于原型上的成员以及应该存在于类本身的成员。
-
实例成员
- 每次通过new操作符调用类标识符,都会执行类构造函数。(类构造函数内部的属性都将属于实例属性,因为添加到this的所有内容都将存在于不同的实例上)。
-
原型成员
-
类块中定义的所有内容都将出现在类的原型上。(类标识符有一个prototype属性指向类的原型)。
- 不能在类块中给原型添加原始值或对象。
-
-
可以在类上定义静态方法。
-
静态类方法常用于执行不特定于实例的操作。
- 静态成员每个类只能有一个,在静态成员中,this引用类自身。(静态成员使用static关键字作为前缀)。
-
-
类继承(ES6新增)
-
ES6的类支持单继承。(单继承是指一个子类只能有一个父类)。
- 使用extends关键字可以继承任何拥有[[Construct]]和原型的对象。(这意味着不仅可以继承一个类,也可以继承一个构造函数,因为构造函数拥有[[Construct]]和原型)。
XMind: ZEN - Trial Version