简单介绍:

说明: Js中的对象似乎无序属性的集合,其属性可以包含基本值,对象,函数,严格的说也就是对象是一组没有特定顺序的键值对,每个对象都是基于一个引用类型创建


/*
 * 创建对象,强烈推荐字面量式创建
 */
var person = {
    name: '李满满',
    age: 25,
    job: '运维开发工程师',
    sayInfo: function(){
        var info = '姓名 : '+this.name+'\n'+
                   '年龄 : '+this.age+'\n'+
                   '工作 : '+this.job
        alert(info)
    }
}
person.sayInfo()


属性特性:

说明: 对象的数据属性或访问属性特性的设置都是使用Object.defineProperty(对象, 对象属性, 属性特性对象)方法,其中属性特性对象可以使用{}包含多个属性特性键值对,如果想一次性设置多个属性特性以及属性类型可使用Object.defineProperties(对象, 属性对象),属性对象可以使用{}包含多个,并对可以使用{}设置多个属性对象的的属性特性对象

数据特性
configurable 表示是否能修改或删除属性,默认值为true,如果设置为false则以下特性将完全失效
enumerable 表示是否能通过for-in循环返回属性,默认为true
writable 表示是否能够更改属性的值,默认为true
value 包含属性的数据值,默认为undefined
/*
 * 数据属性, 数据属性包含一个数据值,数据值可以被读取和写入.
 */
var person = {
    age: 25,
    sex: '男'
}
Object.defineProperty(person, 'age', {
    writable: false,
    value: 25
})
person.age = 26
// 李满满
console.log(person.age)
// 或
Object.defineProperties(person, {
    'age': {
        writable: false,
        value: 25
    },
    'sex': {
        writable: false,
        value: '男'
    }
})
person.sex = '女'
// 男
console.log(person.sex)
访问特性
configurable 表示是否能修改或删除属性,默认值为true,如果设置为false则以下特性将完全失效
enumerable 表示是否能通过for-in循环返回属性,默认为true
get 在读取属性时调用的函数,默认为undefined
set 在写入属性时调用的函数,默认为undefined
/*
 * 访问属性, 不包含数值,包含getter读取函数和setter设置函数
 */
var money = {
    total: 5000
}
Object.defineProperties(money, {
    add: {
        set: function(num){
            this.total += num
        }
    },
    all: {
        get: function(){
            return this.total
        }
    }
})
console.log(money.all)
// 金钱加1000000000
money.add = 1000000000
console.log(money.all)

注意: Object.defineProperty和Object.defineProperties都可以设置属性特性,但是设置访问属性特性时有所区别,Object.defineProperty中的属性名称不能和原属性名称相同,不然出现递归查询错误,而且访问或设置时必须通过Object.defineProperty定义的属性对象名访问


获取特性:

说明: 如上通过Object.defineProperty或Object.defineProperties来设置对象属性特性,其实还可以通过Object.getOwnPropertyDescriptor(对象, 属性名)获取给定属性的描述符,可以通过此描述符对象获取属性对象对应的值


/*
 * 访问特性, 访问对象指定属性特性
 */
var person = {
    age: 25,
    sex: '男'
}
// 获取属性描述符
var descriptor = Object.getOwnPropertyDescriptor(person, 'age')
// 设置属性特性列表
var properties = ['configurable', 'enumerable', 'writable', 'value']
// 遍历属性列表获取属性特性
var propertiesVals = properties.map(function(item, index, properties){
    return descriptor[item]
})
console.log(propertiesVals)


创建对象:

说明: 通过字面量{}创建对象有明显的缺点,如要创建男女两对象,两个对象都有姓名/年龄/职业等属性,而你需要创建两次而且每次必须写重复的属性甚至属性值,为解决这个问题,出现了多种面向对象的程序设计模式.

工厂函数模式

说明: 工厂模式是抽象了创建对象的过程,由于Js中无法创建类,于是开发人员就使用一种函数来封装以特定的接口创建对象的细节,主要是为了解决创建多个相似对象的问题,但是却无法获取返回对象的类型,于是就出现了构造函数模式


function createPersion(name, age, sex){
    var o = new Object()
    o.name = name
    o.age = age
    o.sex = sex
    o.sayName = function(){
        alert(this.name)
    }
    return o
}
// 根据参数的不同,函数返回不同的实例对象
var limanman = createPersion('李满满', 25, '男')
var liuzheng = createPersion('刘珍珍', 24, '女')
// 如果定义多个工厂函数,而它们的类型都是Object,是不是让很看了很懵逼~
console.log(
    limanman, liuzheng,
    limanman instanceof Object,
    liuzheng instanceof Object)

构造函数模式

说明: 构造函数可以用来创建特定类型的对象,new创建引用对象时运行环境就是定义的对象,所以可以通过new关键字加构造函数来实现类似Array那样初始化对象的属性和方法,但是每个方法都要在每个实例上创建一遍,于是就出现了原型模式


function Person(name, age, sex){
    this.name = name,
    this.age = age,
    this.sex = sex
    this.sayName = function(){
        alert(this.name)
    }
}
// 使用new的函数对象都作为构造函数(this指的是赋值的对象),否则为普通函数(this指的是window对象)
var limanman = new Person('李满满', 25, '男')
var liuzheng = new Person('刘珍珍', 25, '男')
// 如果定义多个不同名称的构造函数,则可以实现和工厂函数类似但可以区分构造出来的对象类型
console.log(
    limanman, liuzheng,
    limanman instanceof Person,
    liuzheng instanceof Person)

注意: 虽然针对于每个方法都要在每个实例上创建一遍可以通过在外部定义一个函数,然后在构造函数内部引用,但是如果构造函数内部方法都靠全局函数传递,则此模式就失去了面向对象的意义

原型对象模式

说明: 每个函数都有一个prototype属性,指向的是对象原型,原型对象可以让所有对象实例共享它所包含的属性和方法,也就是不必在构造函数中定义对象实例的信息,而可以将这些信息直接添加到原型对象中


var Person = function(){
    
}
// 其实原型对象相当于实例对象的基类,所有的公共属性和方法在基类中继承下去
Person.prototype.name = '李满满'
Person.prototype.age = 25
Person.prototype.sex = '男'
Person.prototype.sayName = function(){
    alert(this.name)
}
// 实例化对象默认自带原型对象的属性和方法
var limanman = new Person()
// 实例化对象默认自带原型对象的属性和方法
var liuzheng = new Person()
// true
console.log(limanman.sayName == liuzheng.sayName)

1. 创建函数时自动为函数创建prototype属性,此属性指向函数的原型对象,原型对象有一个内置属性constructor指向构造函数,原型对象支持自定义属性和方法,构造函数实例化后的实例对象将自动继承原型对象中定义的属性和方法,而且可以通过p.isPrototypeOf(obj)或Object.getPrototypeOf(obj)来判断原型对象是否是指定对象的原型.


var Person = function(){
    
}
// 其实原型对象相当于实例对象的基类,所有的公共属性和方法在基类中继承下去
Person.prototype.name = '李满满'
Person.prototype.age = 25
Person.prototype.sex = '男'
Person.prototype.sayName = function(){
    alert(this.name)
}
// 实例化对象默认自带原型对象的属性和方法
var limanman = new Person()
// 实例化对象默认自带原型对象的属性和方法
var liuzheng = new Person()
// true
console.log(limanman.sayName == liuzheng.sayName) 
console.log(
    // 通过原型对象.isPrototypeOf(实例对象)判断原型对象是不是对应实例对象的原型对象
    Person.prototype.isPrototypeOf(limanman),
    Person.prototype.isPrototypeOf(liuzheng),
    // 通过Object.getPrototypeOf(实例对象)获取实例对象的原型对象然后和原型对象判断
    Object.getPrototypeOf(limanman) == Person.prototype,
    Object.getPrototypeOf(liuzheng) == Person.prototype
)

2. 由于访问对象属性时先查找实例对象,然后查找原型对象,所以为实例对象设置与原型对象同名的属性时,访问时原型对象同名属性将被遮盖,只有delete 实例对象.属性删除实例属性才能访问到原型对象属性


var Person = function(){
    
}
// 其实原型对象相当于实例对象的基类,所有的公共属性和方法在基类中继承下去
Person.prototype.name = '李满满'
Person.prototype.age = 25
Person.prototype.sex = '男'
Person.prototype.sayName = function(){
    alert(this.name)
}
// 实例化对象默认自带原型对象的属性和方法
var limanman = new Person()
// 实例化对象默认自带原型对象的属性和方法
var liuzheng = new Person()
// true
console.log(limanman.sayName == liuzheng.sayName)
// delete删除的是实例对象属性,而无法删除原型对象属性,所以最后访问原型对象属性时依然是李满满
liuzheng.name = '刘珍珍'
// 刘珍珍
alert(liuzheng.name)
delete liuzheng.name
// 李满满
alert(liuzheng.name)

3. in不仅可以用于for..in语句,属性 in 实例对象/原型对象可以判断属性是否存在于实例对象与原型对象中,但要确定属性是不是只存在于原型对象中需借助obj.hasOwnProperty(attr),当attr只存在于实例对象时返回true,否则返回false


var Person = function(){
    
}
// 其实原型对象相当于实例对象的基类,所有的公共属性和方法在基类中继承下去
Person.prototype.name = '李满满'
Person.prototype.age = 25
Person.prototype.sex = '男'
Person.prototype.sayName = function(){
    alert(this.name)
}
// 实例化对象默认自带原型对象的属性和方法
var limanman = new Person()
// 实例化对象默认自带原型对象的属性和方法
var liuzheng = new Person()
// true
console.log(limanman.sayName == liuzheng.sayName)
function hasPrototypeProperty(obj, attr){
    return attr in obj && !obj.hasOwnProperty(attr)
}
liuzheng.name = '刘珍珍'
// false
alert(hasPrototypeProperty(liuzheng, 'name'))
delete liuzheng.name
// true
alert(hasPrototypeProperty(liuzheng, 'name'))

4.获取对象所有属性不仅可用for...in遍历,Object.keys(obj)也可以获取所有obj对象的所有可枚举属性,Object.getOwnPropertyNames(obj)可以获取对象的所有属性包括不可枚举属性,返回值都是一个数组


var Person = function(){
    
}
// 其实原型对象相当于实例对象的基类,所有的公共属性和方法在基类中继承下去
Person.prototype.name = '李满满'
Person.prototype.age = 25
Person.prototype.sex = '男'
Person.prototype.sayName = function(){
    alert(this.name)
}
// 实例化对象默认自带原型对象的属性和方法
var limanman = new Person()
// 实例化对象默认自带原型对象的属性和方法
var liuzheng = new Person()
// true
console.log(limanman.sayName == liuzheng.sayName)
// 获取原型对象的可枚举属性
alert(Object.keys(Person.prototype))
// 获取所有的属性,包括不可枚举属性,例如constructor
alert(Object.getOwnPropertyNames(Person.prototype))
// 获取实例对象的所有可枚举属性
alert(Object.keys(limanman))
alert(Object.keys(liuzheng))

5. 原型对象的定义支持更简单的字面量定义,但是需要注意的是字面量中必须包含一个不可枚举的属性constructor并且它必须指向构造函数,当然如果习惯上面每次定义都需要重写一遍Person.prototype的话那就无所谓


var Person = function(){
    
}
// 重写原型对象,需要单独设置一个不可枚举的指向构造函数的constructor属性
Person.prototype = {
    name: '李满满',
    age: 25,
    sex: '男',
    sayName: function(){
        alert(this.name)
    }
    
}
// 通过defineProperty为原型对象定义一个带有值和不可枚举特性的constructor属性
Object.defineProperty(Person.prototype, 'constructor', {
    value: Person,
    Enumerable: false
})
// 实例化对象默认自带原型对象的属性和方法
var limanman = new Person()
// 实例化对象默认自带原型对象的属性和方法
var liuzheng = new Person()
// true
console.log(limanman.sayName == liuzheng.sayName)

6. 原型对象的属性和方法的修改会立即反馈在实例对象上,一旦构造函数实例化后原型对象prototype也就被确定,读取原型对象属性时永远是读取实例化时生成的原型对象地址,所以要么先重写原型对象然后再实例化要么直接在原原型对象上修改,这样才可以保证修改立即反馈到实例对象上


var Person = function(){
    
}
// 首先生成实例化对象, 默认prototype属性指向堆中原型对象
var limanman = new Person()
// 然后给原型对象设置一个addr属性
Person.prototype.addr = '杭州'
alert(limanman.addr)
// 然后重写原型对象,需要单独设置一个不可枚举的指向构造函数的constructor属性
Person.prototype = {
    name: '李满满',
    age: 25,
    sex: '男',
    sayName: function(){
        alert(this.name)
    }
    
}
// 通过defineProperty为原型对象定义一个带有值和不可枚举特性的constructor属性
Object.defineProperty(Person.prototype, 'constructor', {
    value: Person,
    Enumerable: false
})
// 由于Person.prototype被指向了另一个原型对象,而访问时访问的还是之前默认的原型对象所以访问不到
alert(limanman.name)

7. 原生对象的大部分属性和方法也是来自于原型,这也就是说我们可以随时自定义原生对象的属性和方法,但是定义的方法必须接受一个参数,这个参数其实就字面量对象本身,但是并不推荐这样去做


console.log(Object.getOwnPropertyNames(String.prototype))
console.log(Object.getOwnPropertyNames(Array.prototype))

8. 原型对象的值一旦固定,所有的实例对象都具有相同的属性和属性值,也就是说所有的属性和属性值在多个实例之间共享,但是如果原型对象的一个属性的值为引用类型值,则也就代表所有的实例都可以修改这个引用类型值,而我们初衷只是想简单的共享一个引用类型值


function Person(){
    
}
Person.prototype = {
    constructor: Person,
    name: ['李满满'],
    age: 25,
    sex: '男',
    sayName: function(){
        return this.name
    }
}
Object.defineProperties(Person.prototype, {
    constructor: {
        value: Person,
        Enumerable: false
    }
})
var limanman = new Person()
limanman.name.push('李道伟')
// 李满满, 李道伟
alert(limanman.name)

两种组合模式:

说明: 创建自定义类型的最常用的方式是组合使用构造函数模式与原型模式,构造函数模式定义实例属性,原型模式定义方法和共享的属性,这样每个实例都会有自己的一份实例属性的副本,但又同时又共享着原型对象的属性和方法,最大限度的节省内存,还有一个优点是还支持构造函数传递参数


// 构造函数中定义实例对象的属性
function Person(name, age, sex){
    this.name = name,
    this.age = age,
    this.sex = sex
    // 每个实例对象中此属性相互隔离
    this.circle = [0, 1]
}
// 原型对象中定义实例对象的方法
Person.prototype = {
    constructor: Person,
    sayName: function(){
        return this.name
    }
}
Object.defineProperties(Person.prototype, {
    constructor: {
        value: Person,
        Enumerable: false
    }
})
var limanman = new Person('李满满', 25, '男')
limanman.circle.push(2)
// [0, 1, 2]
alert(limanman.circle)
var liuzheng = new Person('刘珍珍', 24, '女')
// [0, 1]
alert(liuzheng.circle)


对象继承:

说明: Js中的继承是基于原型链实现,也就是将要子类型的构造函数的原型对象指向父类型实例对象,这样子类型对象不仅拥有父类型实例对象的属性方法还拥有父类型的原型对象的属性和方法.


// 声明动物父类型
function Animal(){
    
}
// 重写父类型原型对象
Animal.prototype = {
    bark: function(){
        console.log('wang wang wang!')
    }
}
// 声明哈士奇子类型
function Husky(name){
    this.name = name
}
// 用父类型实例对象重写子类型的原型对象
Husky.prototype = new Animal()
// 实例化子类型后发现子类型对象拥有了父类型对象中的属性和方法
var dog = new Husky('小黑')
// wang wang wang!
dog.bark()

注意: 通过原型链实现的继承,当使用instanceof表达式测试时,它同时是Object/父类型/子类型的实例,当然也可以使用p.isPrototypeOf(obj)来判断Object/父类型/子类型.prototype是否是实例的原型对象

扩展: 通过原型链实现的继承,有时需要重写超类型中的某个方法,或是需要添加超类型中不存在的某个方法,但是不管怎么样,给原型添加方法的代码一定要放在超类型实例化对象前,替换原型语句之后.

问题: 原型链实现继承很强大,但是所有继承自该父类型的子类型实例共享父类型实例属性和方法,并没有实现隔离,导致任何一个子类型实例都可以修改继承下来的引用类型值的属性

借用构造函数:

说明: 为了解决原型链继承导致继承的引用类型值在子类型实例对象之间共享带来的问题,可以使用借用构造函数,也就是在子类型构造函数中调用超类型构造函数,并且执行环境设置为this,这样此时的this就代表的是子类型实例化时的对象,这样不同的实例之间继承项就相互隔离了


// 声明动物父类型
function Animal(category){
    this.categories = ['shiba', category]
}
// 声明哈士奇子类型
function Husky(name){
    Animal.call(this, 'husky'),
    this.name = name
}
var dog = new Husky('小黑')
console.log(dog.name+ '-->' +dog.categories)

注意: 借用构造函数方法都在构造函数中定义,函数无法复用,而且超类原型中定义的方法不会继承给子类型,也就说所有类型都只能使用构造函数中定义的属性和方法

两种组合继承:

说明: 组合继承就是将原型链和借用构造函数结合一起使用,使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样即通过在原型上定义方法实现了函数复用,又保证每个实例有自己的属性


// 声明动物父类型
function Animal(){
    this.categories = ['husky']
}
Animal.prototype.sayCategories = function(){
    return '品种:' + this.categories
}
// 声明哈士奇子类型
function Husky(name){
    Animal.call(this),
    this.name = name
}
Husky.prototype = new Animal()
var dog = new Husky('小黑')
console.log(dog.name+ '-->' +dog.sayCategories())