第6章 面向对象的程序设计
ECMAScript中没有类的概念;
1.创建对象-历史
1.1 创建实例,添加方法和属性 → 对象字面量
缺点: 使用同一接口创建很多对象,产生大量重复代码
var person = new Object()
person.name = "Y"
person.age = 18
person.job = "police"
person.sayName = function() {
alert(this.name)
}
↓
var person = {
name = "Y",
age = 18,
job = "police",
sayName = function() {
alert(this.name)
}
}
1.2 工厂模式 (返回对象)
缺点: 没有解决对象识别的问题:怎样知道一个对象的类型;
特点: 内部创建对象,并返回
function createPerson(name, age, job) {
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
alert(this.name)
}
return o
}
1.3 构造函数模式
缺点: 每个方法都要在每个实例上重新创建一遍!不同实例上的同名函数是不相等的,然而,创建2个完成同样任务的Function实例的确没有必要。
特点:
没有显示创建对象;
没有return;
将方法属性赋值给this对象;
首字母大写;
构造函数创建的实例标识为一种特定的类型
1.3.1 理解面试题
调用构造函数创建对象,实际会经历以下4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋值给新对象(this指向这个新对象)
- 执行构造函数代码(为这个新对象添加属性方法)
- 返回新对象
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () { // 每定义一个函数,就是实例化一个对象
alert(this.name)
}
}
var p1 = new Person("Y",18,"police")
var p2 = new Person("H",8,"teacher")
// p1.constructor == Person
// p2.constructor == Person
// ( p1 instanceOf Object ) true
// ( p1 instanceOf Person ) true
// ( p2 instanceOf Object ) true
// ( p2 instanceOf Person ) true
- 在全局作用域调用函数,this总是指向GLOBAL对象
function person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
alert(this.name)
}
}
person('Leo', 16, 'doctor')
window.sayName() //leo
1.3.2 将构造函数当做函数
任何函数,只要通过new操作符来调用,便可以作为构造函数;若不用,即与普通函数无异。this会指向Global对象(在浏览器中就是window)
1.3.3 将方法定义到构造函数外部
缺点: 在全局作用域上定义的函数,若只能被某个对象调用,不合理;并且,如果对象需要定义很多方法,则需要定义很多个全局函数,对于这个自定义的引用类型就丝毫没有封装性可言。
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
}
function sayName() {
alert(this.name)
}
1.4 原型模式
1.4.1 理解原型模式
function Person() {}
Person.prototype.name = "Y"
Person.prototype.age = 18
Person.prototype.job = "police"
Person.prototype.sayName = function () {
alert(this.name)
}
- 使用原型对象的好处:让所有的对象实例共享它所包含的属性和方法;
- 只要创建了一个新函数,就会为该函数创建一个prototype属性,指向函数的原型对象;
- 原型对象会自动获得constructor属性,该属性包含一个指向prototype属性所在函数的指针(constructor属性指向构造函数);
- 对象的constructor属性最初是用来表示对象类型的;
- 构造函数创建的实例,实例内部包含一个指针,指向构造函数的原型对象;
Person.prototype.isPrototypeOf(p1) // true
Object.getPrototypeOf(p1) == Person.prototype // true
- 代码读取某个对象某个属性时,先搜索对象实例,若无再搜索原型对象;
- 若实例中添加的属性和原型属性同名,会屏蔽原型中的属性(因为实例只能访问原型中的值,而不能重写);
- 若将实例中的同名属性设为null,并不能恢复与原型的连接,需要使用delete操作符完全删除实例属性;
p1.hasOwnProperty("name")// true hasOwnProperty()方法可以检测一个属性是存在于实例中还是存在于原型,若来自实例则返回true
1.4.2 in操作符
in操作符会在通过对象能访问给定属性时返回true,无论属性存在于实例还是原型中
"name" in p1 // true
1.4.3 确认属性存在于原型
function hasPrototypeProperty(object,name) {
return !object.hasOwnProperty(name) && (name in object)
}
1.4.4 更简单的原型语法
缺点: constructor属性不再指向Person,切断了实例和原型对象的联系;对包含引用类型值的属性,被实例共享会造成问题。
特点: 减少不必要的输入(每添加一个属性/方法就要多敲一遍Person.prototype)
将Person.prototype设置为一个以对象字面量形式创建的新对象,本质上重写了prototype对象(创建函数时自动创建的原型对象),导致constructor属性指向Object构造函数,尽管instanceof还能返回正确的结果。
function Person() {}
Person.prototype = {
name : "Y",
age : 18,
job : "police",
sayName: function () {
alert(this.name)
}
}
p1 instanceof Object // true
p1 instanceof Person // true
p1.constructor == Person // false
p1.constructor == Object // true
增加constructor属性,确保该属性能访问到适当的值。
function Person() {}
Person.prototype = {
constructor : Person,
name : "Y",
age : 18,
job : "police",
sayName: function () {
alert(this.name)
}
}
1.4.5 原生对象的问题
function Person() {}
Person.prototype = {
constructor : Person,
friends:["Yoona","Jessica"], // 数组,引用类型
name : "Y",
age : 18,
job : "police",
sayName: function () {
alert(this.name)
}
}
p1.friends.push("Krystal")
p1.friends == p2.friends
1.4.6 组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。(实例属性中的引用类型互不干扰)
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ["Yoona","Jessica"]
}
Person.prototype = {
constructor : Person,
sayName: function () {
alert(this.name)
}
}
1.4.7 原生对象的原型
通过原生对象的原型,不仅可以取得所有默认方法的引用,也可以定义新方法。但不推荐在产品化的程序中修改原生对象的原型:
如果因为某个实现中缺少某个方法,就在原始对象的原型中添加,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突,而且这样做也可能会意外地重写原生方法。
1.5 梳理
1.6 修改原生对象的原型
String.prototype.startsWith = function(){}
在当前环境中,所有字符串都可以调用startsWith,但不推荐,可能会产生命名冲突,也可能会意外地重写原生方法。
String.prototype.
toString = function () {
console.log('修改原生对象默认方法')
}
'str'.toString() // 修改原生对象默认方法
将原生对象原型指向空,没效果
String.prototype = null
console.log('str'.toString()) // str
2. 属性类型 P139
2.1 数据属性
- [[Configurable]]:能否通过delete删除属性,默认为true
- [[Enumerable]]:能否通过for-in循环返回属性,默认为true
- [[Writeable]]:能否修改属性的值,默认为true
- [[Value]]:属性值,默认为undefined
- 一旦把属性定义为不可配置的,就不能再把它变回可配置了;
- 要修改属性默认的特性,必须使用Object.defineProperty(),3个参数:对象、属性名、描述符对象(属性必须是数据属性),若不指定数据属性,则默认为false;
2.2 访问器属性
- 包含一对getter和setter函数(都不是必须的),有4个特性:
1) [[Configurable]]
2) [[Enum而able]]
3) [[Get]]:读取属性时默认调用的函数,默认值undefined
4) [[Set]]:写入属性时默认调用的函数,默认值undefined
- 属性 _year 前面的下划线用于表示只能通过对象方法访问的属性;
- 使用访问器属性的常见方式:设置一个属性的值会导致其他属性发生变化;
2.3 读取属性
Object.getOwnPropertyDescriptor(),2个参数:对象、属性
var descriptor = Object.getOwnPropertyDescriptor(person,"age")
alert(age.value)
alert(age.enumerable)
alert(age.configurable)
alert(age.get)
方法集合
Object.defineProperty() // 修改属性默认值
Object.getOwnPropertyDescriptor() //读取属性的描述符
谁谁的原型对象.isPrototypeOf(实例) // 参数是实例,判断实例内部是否有指向构造函数原型对象的指针
hasOwnProperty() // 检测属性来自实例(返回true)还是原型
3. 继承(子类继承父类的特征和行为)
3.1 原型链
原型链的问题
- P166 原型链虽然很强大,可以用它来实现继承,但它也存在一些问题,其中最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享,因此要在构造函数,而不是原型对象中定义属性。
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。(没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数)
让原型对象等于另一个类型的实例。
SubType.prototype = new SuperType()
SubType的实例指向SubType的原型,进而又指向SuperType的原型
3.2 默认的原型
所有引用类型默认都继承了Object,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype,这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。完整的原型链如下:
instance instanceof Object // true
instance instanceof SuperType // true
instance instanceof SubType // true
Object.prototype.isPrototypeOf(instance) // true
SuperType.prototype.isPrototypeOf(instance) // true
SubType.prototype.isPrototypeOf(instance) // true
function Fun() {}
console.log(Fun.prototype.__proto__==Object.prototype) // true
3.3 组合继承
- 父构造函数内有引用类型属性值
- 子构造函数内使用call(保证了实例不会都指向同一个引用类型)
- 子构造函数原型指向父构造函数原型(
Object.create
) - 子构造函数原型里的构造器指向自己(知晓实例由谁创建)
4. 方法整理
- A.isPrototypeOf(a)
// 实例a的__proto__指向A的原型
// 判断原型对象A是否是实例a的原型,是则返回true- a.hasOwnProperty(‘name’) 判断属性是否存在于实例中(实例属性),是则返回true
Object.keys() 获得对象上所有可枚举的属性- ‘name’ in a 无论原型/实例,只要是能访问得到的属性,in操作符返回true(包括constructor)'name’字符串
for-in循环,返回所有能通过对象访问的、可枚举的属性 ,(遍历一个实例对象,原型上的属性也会打印、要只打印实例属性,需要配合hasOwnProperty)- Object.getOwnPropertyNames(),得到所有实例属性,包括constructor
function Test(name) {
this.name = name
}
Test.prototype.name = 'hhh'
let tObj = new Test('yyy')
console.log('name' in tObj) // true
console.log('constructor' in tObj) // true
for(var prop in tObj){
console.log('for-in', prop) // name
}