《JavaScript 面向对象精要》 阅读摘要

高程面向对象这块内容介绍的比较浅显,我的以为这本小书是高程的补充,看完以后以为收获匪浅,因此作了个笔记,以备后询javascript

1. 原始类型和引用类型

Js中两种基本数据类型:原始类型(基本数据类型)和引用类型
原始类型保存为简单数据值,引用类型则保存为对象,其本质是指向内存位置的应用。
其它编程语言用栈存储原始类型,用堆存储引用类型,而js则不一样:它使用一个变量对象追踪变量的生存期。原始值被直接保存在变量对象里,而引用值则做为一个指针保存在变量对象内,该指针指向实际对象在内存中的存储位置。java

1.1 原始类型(基本数据类型)

Js中一共有5种原始类型:booleannumberstringnullundefined,除了null类型,均可以用typeof来判断
原始类型的变量直接保存原始值(而不是一个指向对象的指针),当原始值被赋给一个变量,该值将被复制到变量中,每一个变量有它本身的一份数据拷贝编程

var color1='red',color2=color1
console.log(color1)    // red
console.log(color2)    // red
color1='blue'
console.log(color2)    // red

1.2 引用类型

对象(引用值)是引用类型的实例。对象是属性的无序列表,属性包含键和值,若是一个属性的值是函数,它就被称为方法
Js中的函数实际上是引用值,除了函数能够运行之外,一个包含数组的属性和一个包含函数的属性没什么区别。
Js中的构造函数用首字母大写来跟非构造函数区分:var object = new Object()
由于引用类型不在变量中直接保存对象,因此object变量实际上并不包含对象的实例,而是一个指向内存中实际对象所在位置的指针。api

var object1 = new Object()
var object2 = object1

一个变量赋值给另外一个变量时,两个变量各得到一个指针的拷贝,而且指向同一个内存中的对象实例。
对象不使用时能够将引用解除:object = null,内存中的对象再也不被引用时,垃圾收集器(GC)会把那块内存挪做他用,在大型项目中尤其重要数组

1.3 原始封装类型

原始封装类型共3种:StringNumberBoolean,使用起来跟对象同样方便,当读取这三种类型时,原始封装类型将被自动建立:安全

var name = "Nicholas"
var fisrtChar = name.charAt(0)
console.log(firstChar)                        // N

背后发生的故事:闭包

// what js engine does
var name = "Nicholas"
var temp = new String(name)            // 字符串对象
var firstChar = temp.charAt(0)
temp = null
console.log(firstChar)                         // N

Js引擎建立了一个字符串的实例让charAt(0)能够工做,字符串对象的存在仅用于该语句而且在随后被销毁(一种被称为自动打包的过程)。能够测试:app

var name = "Nicholas"
name.last = "zakas"
console.log(name.last)                // undefined

原始封装类型的属性会消失是由于被添加属性的对象马上就被销毁了。
背后的故事:编程语言

var name = "Nicholas"
var temp = new String(name)
temp.last = "zakas"
temp = null                                            // temp对象销毁

var temp = new String(name)
console.log(temp.last)                           // undefined
temp = null

其实是在一个马上就被销毁的临时对象上而不是字符串上添加了新的属性,以后试图再访问该属性,另外一个不一样的临时对象被建立,而新属性并不存在。虽然原始封装类型会被自动建立,在这些值上进行的instanceof检查对应类型的返回值倒是falseide

var name = 'Nicholas', count = 10, found = false
console.log(name instanceof String)                    // false
console.log(count instanceof Number)                // false
console.log(found instanceof Boolean)                // false

这是由于临时对象仅在值(属性)被读取时被建立,instanceof操做符并无真的读取任何东西,也就没有临时对象的建立。
若是使用手动建立对象和原始封装类型之间有必定区别,好比:

var found = new Boolean(false)
if (found) {
    console.log("Found")            // 执行了,由于对象在if条件判断时总被认为是true,不管该对象是否是false,因此尽可能避免手动建立原始封装类型
}

2. 函数

使函数不一样于其它对象是函数存在一个[[Call]]的内部属性。内部属性没法经过代码访问而是定义了代码执行时的行为。ECMAScript为Js的对象定义了多种内部属性,这些内部属性都用[[ ]]来标注。[[Call]]属性代表该对象能够被执行,因为仅函数拥有该属性,ECMAScript定义typeof操做符对任何具备[[Call]]属性的对象返回function

2.1 函数声明与函数表达式

函数有两种字面形式,函数声明函数表达式,二者有个很是重要的区别,函数声明会被提高至上下文的顶部(要么是函数声明时所在函数的范围,要么是全局范围),这意味着能够先使用再声明函数。

2.2 函数就是值

函数能够像使用对象同样使用,能够将它们赋给变量,在对象中添加它们,将它们当成参数传递给别的函数,或从别的函数中返回,基本上只要是可使用其它引用值的地方,就可使用函数。

2.3 参数

函数的参数实际上被保存在一个arguments的数组中,arguments能够自由增加来包含任意个数的值,它的length属性能够告诉当前有多少个值。
arguments对象自动存在于函数中。也就是说函数的命名参数不过是为了方便,并不真的限制了函数可接受参数的个数。

注意: arguments对象不是一个数组的实例,其拥有的方法与数组不一样, Array.isArray(arguments)返回 false

函数指望的参数个数保存在函数的length属性中。

2.4 重载

Js中不存在签名,所以也不存在重载,声明的同名函数后一个会覆盖前一个。
不过能够对arguments对象获取的参数个数进行判断来决定怎么处理。

2.5 对象方法

能够像添加属性那样给对象添加方法,注意定义数据属性和方法的语法彻底相同。

var person = {
    name: "Nicholas",
    sayName: function () {
        console.log(person.name)
    }
}

2.5.1 this对象

以前的例子的sayName()直接引用了person.name,在方法和对象之间创建了紧耦合,这种紧耦合使得一个方法很难被不一样对象使用。
Js全部函数做用域内都有一个this对象表明该函数的对象。在全局做用域内,this表明全局对象window,当一个函数做为对象的方法被调用时,默认this的值等于那个对象。改写:

var person = {
    name: "Nicholas",
    sayName: function () {
        console.log(this.name)  
    }
}

因此应该在方法内引用this而不是直接引用对象。能够轻易改变变量名,或者将函数用在不一样对象上,而不用大量改动代码。

function sayNameForAll() {
    console.log(this.name)
}
var person1={
    name: "Nicholas",
    sayName: sayNameForAll
}
var person2={
    name: "Greg" ,
    sayName: sayNameForAll
}
var name = "Micheal"
person1.sayName()                            // Nicholas
person2.sayName()                            // Greg
sayNameForAll()                                // Micheal

this在函数被调用时才被设置,所以最后sayNameForAll函数执行时的this为全局对象。

2.5.2 改变this

有3种方法能够改变this,函数是对象,而对象能够有方法,因此函数也有方法。

call()

第一个用于操做this的方法是call(),它以指定的this和参数来执行函数,第一个参数为函数执行时的this的值,后面的参数为须要被传入函数的参数。

function sayNameForAll (label) {
    console.log(label + ':' + this.name)
}
var person1 = {name: "Nicholas"}
var person2 = {name: "Greg"}
var name = "Micheal"
sayNameForAll.call(this,"global")                        // global:Micheal
sayNameForAll.call(person1, "person1")             // person1:Nicholas
sayNameForAll.call(person2,"person2")              // person2:Greg
apply()

第二个用于操做this的方法时apply(),其工做方式与call()彻底同样,但它只接受两个参数:this的值和一个数组或者相似数组的对象,内含须要被传入函数的参数(能够把arguments对象做为apply的第二个参数)。

function sayNameForAll (label) {
    console.log(label + ":" + this.name)
}
var person1 =  {name:"Nicholas"}
var person2 = {name:"Greg"}
var name = "Micheal"
sayNameForAll.apply(this,["global"])                        // global:Micheal
sayNameForAll.apply(person1, ["person1"])             // person1:Nicholas
sayNameForAll.apply(person2,["person2"])              // person2:Greg

若是你已经有个数组,那么推介使用apply(),若是你有的是单独的变量,则用call()

bind()

改变this的第三个函数方法为bind()bind()的第一个参数是要传给新函数的this的值,其余参数表明须要被永久设置在新函数中的命名参数,能够在以后继续设置任何非永久参数。

function sayNameForAll (label) {
    console.log(label + ":" + this.name)
}
var person1 =  {name:"Nicholas"}
var person2 = {name:"Greg"}

var sayNameForPerson1 = sayNameForAll.bind(person1)
sayNameForPerson1("person1")                                                // person1:Nicholas
var sayNameForPerson2 = sayNameForAll.bind(person2,"person2")
sayNameForPerson2()                                                                // person2:Greg
person2.sayName = sayNameForPerson1;
person2.sayName("person2")                                                    // person2:Nicholas

sayNameForPerson1()没有绑定永久参数,所以能够继续传入label参数输出,sayNameForPerson2()不只绑定了person2做为this,并且绑定了第一个参数为person2,所以可使用sayNameForPerson2()而不用传入额外参数,可是也不能更改了。person2.sayName最后因为this的值在sayNameForPerson1的函数表达式中已经绑定为person1了,因此虽然sayNameForPerson1如今是person2的方法,它依然输出person1.name的值。

3. 理解对象

Js中的对象是动态的,能够在代码执行的任意时刻发生改变。

3.1 定义属性

当一个属性第一次被添加给对象时,Js在对象上隐式调用一个名为[[Put]]的内部方法,[[Put]]方法会在对象上建立一个新节点保存属性,就像第一次在哈希表上添加一个键同样。这个操做不只指定了初试的值,也定义了属性的一些特征。
调用[[Put]]的结果是在对象上建立了一个自有属性,该属性被直接保存在实例内,对该属性的全部操做都必须经过该对象进行。
当一个已有的属性被赋予一个新值时,调用的是一个名为[[Set]]的方法,该方法将属性的当前值替换为新值。

3.2 属性探测

因为属性能够在任什么时候候添加,所以有时候有必要检查对象是否已有该属性:

if(person1.age){            // 不可取
    // 执行
}

问题在于Js的类型强制会影响输出结果,若是if判断的值为null、undefined、0、false、NaN或者空字符串时则判断为假。因为一个对象属性能够包含这些假值,上例代码可能致使错误的判断,更可靠的判断是用in操做符。
in操做符是在给定对象上查找一个给定名称的属性,若是找到则返回true,另外in操做符在判断的时候不会评估属性的值:

var person1={
    name: "Nicholas",
    age: "111",
    sayName:function(){
        consloe.log(this.name)
    }
}
console.log('name' in person1)            // true
console.log('age' in person1)              // true
console.log('title' in person1)              // false
console.log('sayName' in person1)            // true    方法是值为函数的属性,所以一样能够用in判断

可是in操做符会检查自有属性和原型属性,所以在只想要自有属性的时候使用hasOwnProperty()判断一下,该方法在给定的属性存在而且为自有属性时返回true。

3.3 删除属性

正如属性能够在任什么时候候被添加,也能够在任什么时候候被删除。可是设置一个属性值为null并不能将其从对象中删除,只是调用[[Set]]将null替换了该属性原来的值。完全的删除属性值须要delete操做符。
delete操做符针对单个对象调用[[Delete]]的内部方法,能够认为该操做在哈希表中移除了一个键值对,当delete操做符成功时,它返回true。

注意: 某些属性没法被 delete
var person1= {name: 'Nicholas'}
console.log('name' in person1)                // true
delete person.name
console.log('name' in person1)                // false
console.log(person1.name)                          // undefined

3.4 属性枚举

全部你添加的属性默认为可枚举的,能够用for-in循环遍历,可枚举属性的内部特征[[Enumerable]]都被设置为true。for-in循环会枚举一个对象中全部的可枚举属性并将属性名赋给一个对象:

var property
for (property in object){
    console.log('name:' + property)
    console.log('value' + object[property])
}

若是只须要获取一个对象的属性列表,ES5引入了Object.keys()方法,它能够获取可枚举属性的名字(key)的数组。

注意: Object.keys()只返回自有属性不返回原型属性。
var properties = Object.keys(object)
var i, len=properties.length
for (i=0; i<len; i++){
    console.log('name:' + properties[i])
    console.log('value' + object[properties[i]])
}

并非每一个属性都是可枚举的,可使用propertyIsEnumerable()方法检查一个属性是否为可枚举,每一个对象都有该方法。

var person1= {name: 'Nicholas'}
var properties = Object.keys(person1)
console.log('name' in person1)                                                // true
console.log(person1.propertyIsEnumerable('name'))            // true
console.log('length' in properties)                                            // true
console.log(properties.propertiesIsEnumerable('length'))            // false

这里name为可枚举,由于它是person1的自有属性,而propertieslength为不可枚举的,由于它是Array.prototype的内建属性,你会发现不少原生属性默认都是不可枚举的。

3.5 属性类型

属性有两种类型数据属性访问器属性
数据属性包含一个值,例如以前的name属性,[[Put]]方法默认行为是建立一个数据属性。
访问器属性不包含值而是定义了一个当属性被读取时调用的函数getter和一个当属性被写入时调用的函数setter

let person1 = {
    _name: "Nicholas" ,                        // 前置下划线是约定俗成的,表示该属性为私有的,实际上它是公开的
    get name() {
        console.log("reading me")
        return this._name
    },
    set name(val) {
        console.log(`setting name to ${val}`)
        this._name = val
    }
}
console.log(person1.name)                // reading me Nicholas
person1.name='greg'
console.log(person1.name)                // setting name to Greg

用于定义namegettersetter的语法看上去像函数可是没有function关键字,注意getset以后的name须要跟被访问的属性名保持一致。
当你但愿赋值操做会触发一些行为或者读取的值须要经过计算所需的返回值获得时,访问器属性将会颇有用。

注意: 不必定要同时定义 gettersetter,能够选择其中之一,若是只定义 getter,那么属性变为只读,在非严格下写入将失败,严格下写入报错,若是只定义 setter,那么属性为只写,两种模式下读取都失败

3.6 属性特征

ES5以前没法访问属性的任何特征,也没有办法指定一个属性是否为可枚举,所以ES5引入多种方法与属性特征互动,同时也引入新的特征来支持额外的功能,如今已经能够建立出和Js内建属性同样的自定义属性。下面介绍数据属性和访问器属性的特征。

3.6.1 通用特征

有两个属性时数据属性和访问器属性共有的:
[[Enumerable]]决定你是否能够遍历该属性;
[[Configurable]]决定该属性是否可配置;
你能够用delete删除一个可配置的属性,或者随时改变它,也能够把可配置的属性从数据属性变为访问器属性,反之亦可,全部自有属性都是可枚举和可配置的。

若是你想改变属性特征,可使用Object.defineProperty()方法,它接受三个参数:拥有函数的对象、属性名、包含须要设置的特征的属性描述对象。属性描述对象具备和内部特征同名的属性但名字中不包含中括号,因此可使用enumerable属性来设置[[Enumerable]]特征,用configurable属性来设置[[Configurable]]特征。假如你想让一个对象属性变成不可枚举且不可配置:

var person1 = { name: 'Nicholas' }
var properties = Object.keys(person1)

Object.defineProperty(person1, 'name', { enumerable: false })
console.log('name' in person1)                          // true
console.log(person1.propertyIsEnumerable('name'))      // false
console.log(properties.length)                            // 0
Object.defineProperty(person1, 'name', { configurable: false })
delete person1.name                                            // 属性设置为不可配置以后不能被delete,删除失败
console.log('name' in person1)                        // true
console.log(person1.name)                            // Nicholas
Object.defineProperty(person1, 'name', { configurable: true })    // error!    设置为不可配置以后就不能再设置属性特征了,包括[[Configurable]]

3.6.2 数据属性特征

数据属性额外拥有两个访问器属性不具有的特征:
[[Value]]包含属性的值,当你在对象上建立属性时该特征被自动赋值,全部属性的值都保存在[[Value]]中,哪怕该值是一个函数;
[[Writable]]是一个布尔值,指示该属性是否能够写入,全部属性默认都是可写的,除非另外指定。
经过这两个额外属性,可使用Object.defineProperty()完整定义一个数据属性,即便该属性还不存在。

var person1 = { name: 'Nicholas' }                // 等同于
Object.defineProperty(person, 'name',  {
    value: "Nicholas",
    enumerable: true,
    configurable: true,
    writable: true
}

Object.defineProperty()被调用时,它首先检查属性是否存在,若是不存在将根据属性描述对象指定的特征建立。当使用Object.defineProperty()定义新属性时必定记得为全部的特征指定一个值,不然布尔型的特征会被默认设置为false。

var person1 = {}
Object.defineProperty(person1, 'name', { value: 'Nicholas' })    // 因为没有显式指定特征,所以属性为不可枚举、不可配置、不可写的
console.log('name' in person1)                          // true
console.log(person1.propertyIsEnumerable('name'))      // false
delete person1.name
console.log('name' in person1)                    // true
person1.name = 'Greg'
console.log(person1.name)                          // Nicholas
在严格模式下视图改变不可写属性会抛出错误,而在非严格模式下会失败

3.6.3 访问器属性

访问器属性拥有两个数据属性不具有的特征,访问器属性不须要储存值,所以也就没有[[Value]][[Writable]],取而代之的是[[Get]][[Set]]属性,内含gettersetter函数,同字面量形式同样,只须要定义其中一个特征就能够建立一个访问器属性。

若是试图建立一个同时具备数据属性和访问器属性的属性,会报错

以前get set 例子能够被改写为:

let person1 = { _name: "Nicholas" }
Object.defineProperty(person1, 'name', {
    get: function() {
      console.log("reading me")
      return this._name
    },
    set: function(val) {
      console.log(`setting name to ${val}`)
      this._name = val
    },
    enumerable: true,
    configurable: true
  }
)
console.log(person1.name)               // reading me Nicholas
person1.name = 'greg'
console.log(person1.name)                // setting name to Greg

注意Object.defineProperty()中的get和set关键字,它们是包含函数的数据属性,这里不能使用字面量形式。

3.6.4 定义多重属性

若是你使用Object.defineProperties()而不是Object.defineProperty()能够为一个对象同时定义多个属性,这个方法接受两个参数:须要改变的对象、一个包含全部属性信息的对象。后者能够背当作一个哈希表,键是属性名,值是为该属性定义特征的属性描述对象。

var person1 = {}
Object.defineProperties(person1, {
  _name: {
    value: 'Nicholas',
    enumerable: true,
    configurable: true,
    writable: true
  },
  name: {
    get: function() {
      console.log('reading me')
      return this._name
    },
    set: function(val) {
      console.log(`setting name to ${val}`)
      this._name = val
    },
    enumerable: true,
    configurable: true
  }
})

3.6.5 获取属性特征

若是须要获取属性的特征,Js中可使用Object.getOwnPropertyDescriptor(),这个方法只能够用于自有属性,它接受两个参数:对象、属性名。若是属性存在,它会返回一个属性描述对象,内含四个属性:configurable、enumerable、另外两个根据属性类型决定。即便你从没有为属性显式指定特征,你依然会获得包含所有这些特征值的属性描述对象。

3.7 禁止修改对象

对象和属性同样具备指导行为的内部特征,其中,[[Extensible]]是一个布尔值,它指明该对象自己是否能够被修改,你建立的全部对象默认都是可扩展的,新的属性能够随时被添加,设置[[Extensible]]为false则能够禁止新属性的添加。
下面有三种方法能够用来锁定对象属性

3.7.1 禁止扩展

第一种方法是Object.preventExtensions()建立一个不可扩展的对象。该方法接受一个参数:你但愿扩展的对象。一旦在一个对象上用这个方法,就永远不能再给它添加新的属性了。

let person1 = { _name: "Nicholas" }
console.log(Object.isExtensible(person1))            // true
Object.preventExtensions(person1)
console.log(Object.isExtensible(person1))            // false
person1.sayName = function(){
    console.log(this.name)
}
console.log('sayName' in person1)                // false
在严格模式下试图给一个不可扩展对象添加属性会抛出错误,而在非严格模式下会失败。应该对不可扩展对象使用严格模式,这样当一个不可扩展对象被错误使用时你就会知道

3.7.2 对象封印

一个被封印的对象是不可扩展的且其全部属性都不可配置,这意味着不只不能给对象添加属性,并且也不能删除属性或改变类型(从数据属性改变成访问属性或者反之),若是一个对象被封印,那么只能读写它的属性。
能够用Object.seal()方法来封印一个对象,该方法被调用时[[Extensible]]特征被设置为false,其全部属性的[[Configurable]]特征被置为false,可使用Object.isSealed()来判断一个对象是否被封印。
这段代码封印了person1,所以不能再person1上添加或者删除属性。全部的被封印对象都是不可扩展的对象,此时对person1使用Object.isExtensible()方法将会返回false,且视图添加sayName()会失败。
并且虽然person.name被成功改变成一个新值,可是删除它会失败。

确保对被封印的对象使用严格模式,这样当有人误用该对象时,会报错

3.7.3 对象冻结

被冻结的对象不能添加或删除属性,不能修改属性类型,也不能写入任何数据属性。简言而之,被冻结对象是一个数据属性都为只读的被封印对象。
Object.freeze() 冻结对象。
Object.isFrozen() 判断对象是否被冻结。

被冻结对象仅仅只是对象在某个时间点上的快照,用途有限且不多被使用

4. 构造函数和原型对象

4.1 构造函数

构造函数就是用new建立对象时调用的函数,使用构造函数的好处在于全部用同一个构造函数建立的对象都具备一样的属性和方法。
构造函数也是函数,定义的方式和普通函数同样,惟一的区别是构造函数名应该首字母大写,以此区分。

function Person(){}
var person1 = new Person                        // 若是没有要传递给构造函数的参数,括号能够省略
console.log(person1 instanceof Person)        // true
console.log(person1.constructor === Person)        // true

即便Person构造函数没有显式返回任何东西,person1也会被认为是一个新的Person类型的对象,new操做符会自动建立给定类型的对象并返回它们。每一个对象在建立时都会自动拥有一个构造函数属性,其中包含了一个指向其构造函数的引用。那些经过字面量形式或者Object构造函数建立出来的泛用对象,其构造函数属性constructer指向Object;那些经过自定义构造函数建立出来的对象,其构造函数属性指向建立它的构造函数。

虽然对象实例及其构造函数之间存在这样的关系,可是仍是建议使用instanceof来检查对象类型,这是由于构造函数属性能够被覆盖,并不必定彻底准确。
在构造函数中只需简单的给this添加任何想要的属性便可:

function Person(name){
    this.name =  name
    this.sayName() = function(){
        console.log(this.name)
    }
}

在调用构造函数时,new会自动建立this对象,且其类型就是构造函数的类型,构造函数自己不须要返回一个对象,new操做符会帮你返回。

function Person2(name){
    this.name=name
    this.sayName=function(){
        console.log(this.name)
    }
}
var person2=new Person2('sam') 
console.log(person2.name)                    // sam
person2.sayName()                                // sam

每一个对象都有本身的name属性值,因此sayName能够根据不一样对象返回不一样的值。

也能够在构造函数中显式调用 return,若是返回的是一个对象,那么它会替代新建立的对象实例返回,若是返回的是一个原始类型,那么它将被忽略,新建立的对象实例将被返回。

构造函数容许使用一致的方法初始化一个类型的实例,在使用对象前设置好全部的属性,能够在构造函数中使用Object.defineProperty()的方法来帮助初始化。

function Person(name) {
    Object.defineProperty(this, 'name', {
        get: function() {
            return name
        },
        set: function(newName) {
            name = newName
        },
        enumerable: true,
        configurable: true
    })

    this.sayName = function() {
        console.log(this.name)
    }
}

var person1 =new Person('Nicholas')                // 始终确保使用了new操做符,不然就是冒着改变全局对象的风险
console.log(person1 instanceof Person)            // true   
console.log(typeof person1)                               // object
console.log(name)                                              // undefined

当Person不是被new调用时候,构造函数中的this指向全局对象,因为Person构造函数依靠new提供返回值,person1变量为undefined。没有new,Person只不过是一个没有返回语句的函数,对this.name的赋值实际上建立了一个全局对象name。

严格模式下,不经过 new调用Person构造函数会出现错误,这是由于严格模式并无为全局对象设置this,this保持为undefined,而试图给undefined添加属性时都会出错

构造函数容许给对象配置一样的属性,当构造函数并无消除代码冗余,每一个对象都有本身的sayName()方法,这意味着100个对象实例就有100个函数作相同的事情,只是使用的数据不一样。若是全部的对象实例共享同一个方法会更有效率,该方法可使用this.name来访问对应的数据,这就须要用到原型对象

4.2 原型对象

原型对象能够看作对象的基类,几乎全部函数(除了一下内建函数)都有一个名为prototype的属性,该属性是一个原型对象用来建立新的对象实例。
全部建立的对象实例共享该原型对象,且这些对象实例能够访问原型对象的属性。例如,hasOwnProperty()方法被定义在泛用对象Object的原型对象中,但却能够被任何对象当作本身的属性访问。

var book = {title: "the principles of object-oriented js"}
console.log('title' in book)
console.log(book.hasOwnProperty('title'))                        // true
console.log('hasOwnProperty' in book)                            // true
console.log(book.hasOwnProperty('hasOwnProperty'))             // false
console.log(Object.prototype.hasOwnProperty('hasOwnProperty'))            // true

即便book中没有hasOwnProperty()方法的定义,但仍然能够经过book.hasOwnProperty()访问该方法,这是由于该方法存在于Object.prototype中。
可使用这样一个方法来判断一个属性是否为原型属性:

function hasPrototypeProperty(object, name){
    return name in object && !object.hasOwnProperty(name)
}

4.2.1 [[Prototype]]属性

一个对象实例经过内部属性[[Prototype]]追踪其原型对象,该 属性时一个指向该实例使用的原型对象的指针。当你使用new建立一个新的对象时,构造函数的原型对象会被赋给该对象的[[Prototype]]属性 (JS proto 探究.md )。你能够调用Object.getPropertyOf()方法读取[[prototype]]属性的值。

Object.prototype.__proto__ === null
var object={}
Object.getPrototypeOf(object) === Object.prototype                // true
Object.prototype.isPrototypeOf(object)                    // true

任何一个泛用对象(字面量形式或者new Object()),其[[Prototype]]对象始终指向Object.prototype。也能够用isPrototypeOf()方法检查某个对象是不是另外一个对象的原型对象,该方法被包含在全部对象中。

Note:大部分Js引擎在全部对象上都支持一个 __proto__的属性,该属性使你能够直接读写 [[Prototype]]属性。包括Firefox、Safari、Chrome、Node.js

在读取一个对象的属性时,Js引擎会首先在对象的自有属性中查找属性名字,若是找到则返回,若是没有则Js会搜索[[Prototype]]中的对象,若是找到则返回,找不到则返回undefined

var object = {}
console.log(object.toString())                    // [object Object]
object.toString = function() {return "[object Custom]"}
console.log(object.toString())                    // [object Custom]
delete object.toString
console.log(object.toString())                    // [object Object]
delete object.toString
console.log(object.toString())                    // [object Object]

上例能够看出,delete运算符只对只有属性起做用,没法删除一个对象的原型属性。而且也不能够给一个对象的原型属性赋值,对.toString的赋值只是在对象上建立了一个新的自有属性,而不是改变原型属性。

4.2.2 在构造函数中使用原型对象

原型对象的共享机制使得它们成为一次性为全部对象定义全部方法的理想手段,由于一个方法对全部的对象实例作相同的事,没理由每一个实例都要有一份本身的方法。将方法放在原型对象中并使用this方法当前实例是更有效的作法。

function Person(name) {this.name = name}
Person.prototype.sayName = function() {console.log(this.name)};
var person1 = new Person("Nicholas")
console.log(person1.name)                        // Nicholas
person1.sayName()                                // Nicholas

也能够在原型对象上存储其余类型的数据,可是在存储引用值时要注意,由于这些引用值会被多个实例共享,可能你们不但愿一个实例可以改变另外一个实例的值。

function Person(name) {this.name = name}
Person.prototype.favorites = []
var person1 = new Person("Nicholas")
var person2 = new Person("Greg")
person1.favorites.push("pizza")
person2.favorites.push("quinoa")

console.log(person1.favorites)                // ["pizza", "quinoa"]
console.log(person2.favorites)                // ["pizza", "quinoa"]

favorites属性被定义到原型对象上,意味着person1.favoritesperson2.favorites指向同一个数组,你对任意Person对象的favorites插入的值都将成为原型对象上数组的元素。也可使用字面量的形式替换原型对象:

function Person(name) {this.name=name}
Person.prototype= {
    sayName: function() {console.log(this.name)},
    toString: function(){return `[Person ${this.name} ]`}
}

虽然用这种字面量的形式定义原型很是简洁,可是有个反作用须要注意。

var person1 = new Person('Nicholas')
console.log(person1 instanceof Person)                // true
console.log(person1.constructor === Person)                // false
console.log(person1.constructor === Object)                // true

使用字面量形式改写原型对象改写了构造函数的属性,所以如今指向Object而不是Person,这是由于原型对象具备个constructor属性,这是其余对象实例所没有的。当一个函数被建立时,其prototype属性也被建立,且该原型对象的constructor属性指向该函数本身,当使用字面量形式改写原型对象Person.prototype时,其constructor属性将被复写为泛用对象Object。为了不这一点,须要在改写原型对象时手动重置其constructor属性:

function Person(name) {this.name = name}
Person.prototype = {
    constructor: Person,             // 为了避免忘记赋值,最好在第一个属性就把constructor重置为本身
    sayName() {console.log(this.name)},
    toString() {return `[Person ${this.name} ]`}
}

var person1 = new Person('Nicholas')
console.log(person1 instanceof Person)                    // true
console.log(person1.constructor === Person)                // true
console.log(person1.constructor === Object)                // false

构造函数、原型对象、对象实例之间:对象实例和构造函数之间没有直接联系。不过对象实例和原型对象之间以及原型对象和构造函数之间都有直接联系。

这样的链接关系也意味着,若是打断对象实例和原型对象之间的联系,那么也将打断对象实例及其构造函数之间的关系。

4.2.3 改变原型对象

给定类型的全部对象实例共享一个原型对象,因此能够一次性扩充全部对象实例。[[Prototype]]属性只是包含了一个指向原型对象的指针,任何对原型对象的改变都将你可反映到全部引用它的对象实例上。这意味着给原型对象添加的新成员均可以马上被全部已经存在的对象实例使用。

function Person(name) {this.name = name}
Person.prototype = {
    constructor: Person,
    sayName() {console.log(this.name)},
    toString() {return `[Person ${this.name} ]`}
}
var person1 = new Person('Nicholas')
var person2 = new Person('Greg')
console.log('sayHi' in person1)                // false
console.log('sayHi' in person2)                // false
Person.prototype.sayHi = () => console.log("Hi")
person1.sayHi()                // Hi
person2.sayHi()                // Hi

当对一个对象使用Object.seal()Object.freeze()封印和冻结对象的时候是在操做对象的自有属性,没法添加封印对象的自有属性和更改冻结对象的自有属性,可是仍然能够经过在原型对象上添加属性来扩展对象实例:

function Person(name) {this.name = name}
var person1 = new Person("Nicholas")
Object.freeze(person1)
Person.prototype.sayHi = function() {console.log("Hi")};
person1.sayHi()            // Hi

其实,[[Prototype]]是实例对象的自有属性,属性自己person1.[[Prototype]]被冻结,可是指向的值Person.prototype并无冻结。

4.2.4 内建对象的原型对象

全部内建对象都有构造函数,所以也都有原型对象能够去改变,例如要在数组上添加一个新的方法只须要改变Array.prototype便可

Array.prototype.sum = function() {
    return this.reduce((privious, current) => privious + current)
}
var numbers = [1, 2, 3, 4, 5, 6]
var result = numbers.sum()
console.log(result)                    // 21

sum()函数内部,在调用时this指向数组的对象实例numbers,所以this也能够调用该数组的其余方法,好比reduce()。
改变原始封装类型的原型对象,就能够给这些原始值添加更多功能,好比:

String.prototype.capitalize = function() {
    return this.charAt(0).toUpperCase() + this.substring(1)
}
var message = 'hello world!'
console.log(message.capitalize())            // Hello world!

5. 继承

5.1 原型对象链和Object.prototype

Js内建的继承方法被称为原型对象链,又称为原型对象继承。原型对象的属性能够由对象实例访问。实例对象集成了原型对象的属性,由于原型对象也是一个对象,它也有本身的原型对象并继承其属性。这就是原型继承链:对象继承其原型对象,而原型对象继承它的原型对象,以此类推。
全部的对象,包括自定义的对象都继承自Object,除非另有指定。更确切的说,全部对象都继承自Object.prototype,任何以字面量形式定义的对象,其[[Prototype]]的值都被设为Object.prototype,这意味着它继承Object.prototype的属性。

var book = {title: 'a book'}
console.log(Object.getPrototypeOf(book) === Object.prototype)            // true

5.1.1 继承自Object.prototype的方法

前几张用到的几个方法都是定义在Object.prototype上的,所以能够被其余对象继承:

Methods Usage
hasOwnProperty() 检查是否存在一个给定名字的自有属性
propertyIsEnumerable() 检查一个自有属性是否为可枚举
isPrototypeOf() 检查一个对象是不是另外一个对象的原型对象
valueOf() 返回一个对象的值表达
toString() 返回一个对象的字符串表达

这几种方法由继承出如今全部的对象中,当你须要对象在Js中以一致的方式工做时,最后两个尤其重要。

  1. valueOf()
    每当一个操做符被用于一个对象时就会调用valueOf()方法,其默认返回对象实例自己。原始封装类型重写了valueOf()以使得它对String返回一个字符串,对Boolean返回一个布尔,对Number返回一个数字;相似的,对Date对象的valueOf()返回一个epoch时间,单位是毫秒(正如Data.prototype.getTime())。

    var now = new Date                // now.valueOf()  === 1505108676169
    var earlier = new Date(2010,1,1)            // earlier.valueOf() === 1264953600000
    console.log(now>earlier)                // true
    console.log(now-earlier)                 // 240155076169

    now是一个表明当前时间的Date,而earlier是过去的时间,当使用操做符>时,两个对象上都调用了valueOf()方法,你甚至能够用两个Date相减来得到它们在epoch时间上的差值。若是你的对象也要这样使用操做符,你能够定义本身的valueOf()方法,定义的时候你并无改变操做符的行为,仅仅应了操做符默认行为所使用的值。

  2. toString()
    一旦valueOf()返回的是一个引用而不是原始值的时候,就会回退调用toString()方法。另外,当Js指望一个字符串时也会对原始值隐式调用toString()。例如当加号操做符的一边是一个字符串时,另外一边就会被自动转换成字符串,若是另外一边是一个原始值,会自动转换成一个字符串表达(true => "true"),若是另外一边是一个引用值,则会调用valueOf(),若是其返回一个引用值,则调用toString()

    var book = {title: 'a book'}
    console.log("book = " + book)                // "book = [object Object]"

    由于book是一个对象,所以调用它的toString()方法,该方法继承自Object.prototype,大部分Js引擎返回默认值[object Object],若是对这个值不满意能够复写,为此类字符串提供包含跟多信息。

    var book = {title: 'a book',
    toString(){return `[Book = ${this.title} ]`}}
    console.log("book = " + book)                 // book = [Book = a book ]

5.1.2 修改Object.prototype

全部的对象都默认继承自Object.prototype,因此改变它会影响到全部的对象,这是很是危险的。
若是给Obejct.prototype添加一个方法,它是可枚举的,能够粗如今for-in循环中,一个空对象依然会输出一个以前添加的属性。尽可能不要修改Object.prototype。

5.2 对象继承

对象字面量形式会隐式指定Object.prototype为其[[Prototype]],也能够用Object.create()方式显示指定。Object.create()方法接受两个参数:须要被设置为新对象[[Prototype]]的对象、属性描述对象,格式如在Object.defineProperties()中使用的同样(第三章)。

var book = {title: 'a book'}
// ↑ 等价于 ↓
var book = Object.create(Object.prototype, {
    title: {
        configurable: true,
        enumerable: true,
        value: 'a book',
        writable: true
    }
})

第一种写法中字面量形式定义的对象自动继承Object.prototype且其属性默认设置为可配置、可写、可枚举。第二种写法显示使用Object.create()作了相同的操做,两个book对象的行为彻底一致。

var person = {
    name: "Jack",
    sayName: function(){
        console.log(this.name);
    }
}

var student = Object.create(person, {
    name:{value: "Ljc"},
    grade: {
        value: "fourth year of university",
        enumerable: true,
        configurable: true,
        writable: true
    }
});

person.sayName(); // "Jack"
student.sayName(); // "Ljc"

console.log(person.hasOwnProperty("sayName")); // true
console.log(person.isPrototypeOf(student)); // true
console.log(student.hasOwnProperty("sayName")); // false
console.log("sayName" in student); // true

console.log(student.__proto__===person)                                      // true
console.log(student.__proto__.__proto__===Object.prototype)      // true

对象person2继承自person1,也就集成了person1的name和sayName(),然而又经过重写name属性定义了一个自有属性,隐藏并替代了原型对象中的同名属性。因此person1.sayName()输出Nicholas而person2.sayName()输出Greg。
在访问一个对象的时候,Js引擎会执行一个搜索过程,若是在对象实例上发现该属性,该属性值就会被使用,若是没有发现则搜索[[Prototype]],若是仍然没有发现,则继续搜索该原型对象的[[Prototype]],知道继承链末端,末端一般是一个Object.prototype,其[[prototype]]为null。这就是原型链
固然也能够经过Object.create()建立[[Prototype]]为null的对象:var obj=Object.create(null)。该对象obj是一个没有原型链的对象,这意味着toString()valueOf等存在于Object原型上的方法都不存在于该对象上。

5.3 构造函数继承

Js中的对象继承也是构造函数继承的基础,第四章提到:几乎全部的函数都有prototype属性(经过Function.prototype.bind方法构造出来的函数是个例外),它能够被替换和修改。该prototype属性被自动设置为一个继承自Object.prototype的泛用对象,该对象有个自有属性constructor

// 构造函数
function YourConstructor() {}
// Js引擎在背后作的:
YourConstructor.prototype = Object.create(Object.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: YourConstructor,
        writable: true
    }
})
console.log(YourConstructor.prototype.__proto__===Object.prototype)            // true

你不须要作额外工做,Js引擎帮你把构造函数的prototype属性设置为一个继承自Object.prototype的对象,这意味着YourConstructor建立出来的任何对象都继承自Object.prototype,YouConstructor是Object的子类。
因为prototype可写,能够经过改写它来改变原型链:

function Rectangle(length, width) {
    this.length = length
    this.width = width
}
Rectangle.prototype.getArea = function() {return this.length * this.width};
Rectangle.prototype.toString = function() {return `[ Rectangle ${this.length}x${this.width} ]`};

function Square(size) {
    this.length = size
    this.width = size
}
Square.prototype = new Rectangle()
Square.prototype.constructor = Square
Square.prototype.toString = function() {return `[ Square ${this.length}x${this.width} ]`}

var rect = new Rectangle(5, 10)
var squa = new Square(6)
console.log(rect instanceof Rectangle)        // true
console.log(rect instanceof Square)        // false
console.log(rect instanceof Object)        // true
console.log(squa instanceof Rectangle)        // true
console.log(squa instanceof Square)        // true
console.log(squa instanceof Object)        // true
MDN:instanceof 运算符能够用来判断某个构造函数的 prototype 属性是否存在另一个要检测对象的原型链上。

Square构造函数的prototype属性被改写为Rectagle的一个实例,此时不须要给Rectangle的调用提供参数,由于它们不须要被使用,并且若是提供了,那么全部的Square对象实例都会共享这样的维度。若是用这种方式改写原型链,须要确保构造函数不会再参数缺失时抛出错误(不少构造函数包含的初始化逻辑)且构造函数不会改变任何全局状态。

// inherits from Rectangle
function Square(size){
    this.length = size;
    this.width = size;
}

Square.prototype = new Rectangle(); // 尽管是 Square.prototype 是指向了 Rectangle 的对象实例,即Square的实例对象也能访问该实例的属性(若是你提早声明了该对象,且给该对象新增属性)。
// Square.prototype = Rectangle.prototype; // 这种实现没有上面这种好,由于Square.prototype 指向了 Rectangle.prototype,致使修改Square.prototype时,实际就是修改Rectangle.prototype。
console.log(Square.prototype.constructor); // 输出 Rectangle 构造函数

Square.prototype.constructor = Square; // 重置回 Square 构造函数
console.log(Square.prototype.constructor); // 输出 Square 构造函数

Square.prototype.toString = function(){
    return "[Square " + this.length + "x" + this.width + "]";
}

var rect = new Rectangle(5, 10);
var square = new Square(6);

console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36

console.log(rect.toString()); // "[Rectangle 5 * 10]", 但若是是Square.prototype = Rectangle.prototype,则这里会"[Square 5 * 10]"
console.log(square.toString()); // "[Square 6 * 6]"

console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true

Square.prototype 并不真的须要被改为为一个 Rectangle 对象。事实上,是 Square.prototype 须要指向 Rectangle.prototype 使得继承得以实现。这意味着能够用 Object.create() 简化例子。

// inherits from Rectangle
function Square(size){
    this.length = size;
    this.width = size;
}

Square.prototype= Object.create(Rectangle.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: Square,
        writable: true
    }
})
在对原型对象添加属性前要确保你已经改写了原型对象,不然在改写时会丢失以前添加的方法(由于继承是将被继承对象赋值给须要继承的原型对象,至关于重写了须要继承的原型对象)。

5.4 构造函数窃取

因为JavaScript中的继承是经过原型对象链来实现的,所以不须要调用对象的父类的构造函数。若是确实须要在子类构造函数中调用父类构造函数,那就能够在子类的构造函数中利用 call、apply方法调用父类的构造函数。

function Rectangle(length, width) {
    this.length = length
    this.width = width
}
Rectangle.prototype.getArea = function() {return this.length * this.width};
Rectangle.prototype.toString = function() {return `[ Rectangle ${this.length}x${this.width} ]`};

function Square(size) {Rectangle.call(this, size, size)}
Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        value: Square,
        enumerable: true,
        configurable: true,
        writable: true
    }
})
Square.prototype.toString = function() {return `[ Square ${this.length}x${this.width} ]`}

var rect = new Rectangle(5, 10)
var squa = new Square(6)
console.log(rect.getArea())
console.log(rect.toString())
console.log(squa.getArea())
console.log(squa.toString())

通常来讲,须要修改 prototype 来继承方法并用构造函数窃取来设置属性,因为这种作法模仿了那些基于类的语言的类继承,因此这一般被称为伪类继承

5.5 访问父类方法

其实也是经过指定 callapply 的子对象调用父类方法。

6. 对象模式

可使用继承或者混入等其余技术令对象间行为共享,也能够利用Js高级技巧阻止对象结构被改变。

6.1 私有成员和特权成员

6.1.1 模块模式

模块模式是一种用于建立拥有私有数据的单件对象的模式。
基本作法是使用当即调用函数表达式(IIFE)来返回一个对象。原理是利用闭包。

var yourObj = (function(){
    // private data variables   
    return {
        // public methods and properties
    }
}());

模块模式还有一个变种叫暴露模块模式,它将全部的变量和方法都放在 IIFE 的头部,而后将它们设置到须要被返回的对象上。

//  通常写法
var yourObj = (function(){
    var age = 25;    
    return {
        name: "Ljc",      
        getAge: function(){
            return age 
        }
    }
}());

// 暴露模块模式,保证全部变量和函数声明都在同一个地方
var yourObj = (function(){
    var age = 25;                            // 私有变量,外部没法访问
    function getAge(){
        return age
    };
    return {
        name: "Ljc",                          // 公共变量外部能够访问
        getAge: getAge                    // 外部能够访问的对象
    }
}());

6.1.2 构造函数的私有成员

模块模式在定义单个对象的私有属性十分有效,但对于那些一样须要私有属性的自定义类型呢?你能够在构造函数中使用相似的模式来建立每一个实例的私有数据。

function Person(name){
    // define a variable only accessible inside of the Person constructor
    var age = 22;   
    this.name = name;
    this.getAge = function(){return age;};
    this.growOlder = function(){age++;}
}

var person = new Person("Ljc");
console.log(person.age);         // undefined
person.age = 100;
console.log(person.getAge());         // 22
person.growOlder();
console.log(person.getAge());         // 23

构造函数在被new的时候建立了一个本地做用于并返回this对象。这里有个问题:若是你须要对象实例拥有私有数据,就不能将相应方法放在 prototype上。
若是你须要全部实例共享私有数据(就好像它被定义在原型对象里那样),则可结合模块模式和构造函数,以下:

var Person = (function(){
    var age = 22;
    function InnerPerson(name){this.name = name;}
    InnerPerson.prototype.getAge = function(){return age;}
    InnerPerson.prototype.growOlder = function(){age++;};
    return InnerPerson;
}());

var person1 = new Person("Nicholash");
var person2 = new Person("Greg");
console.log(person1.name); // "Nicholash"
console.log(person1.getAge()); // 22
console.log(person2.name); // "Greg"
console.log(person2.getAge()); // 22

person1.growOlder();
console.log(person1.getAge()); // 23
console.log(person2.getAge()); // 23

6.2 混入

这是一种伪继承。一个对象在不改变原型对象链的状况下获得了另一个对象的属性被称为“混入”。所以,和继承不一样,混入让你在建立对象后没法检查属性来源。

function mixin(receiver, supplier){
    for(var property in supplier){
        if(supplier.hasOwnProperty(property)){
            receiver[property] = supplier[property];
        }
    }
}

这是浅拷贝,若是属性的值是一个引用,那么二者将指向同一个对象。
要注意一件事,使用这种方式,supplier的访问器属性会被复制为receiver的数据属性。

function mixin(reciver, supplier) {
    if (Object.getOwnPropertyDescriptor) {                    // 检查是否支持es5
        Object.keys(supplier).forEach(property => {
            var descriptor = Object.getOwnPropertyDescriptor(supplier, property)
            Object.defineProperty(reciver, property, descriptor)
        })
    } else {
        for (var property in supplier) {                        // 不然使用浅复制
            if (supplier.hasOwnProperty(property)) {
                reciver[property] = supplier[property]
            }
        }
    }
}

6.3 做用域安全的构造函数

构造函数也是函数,因此不用 new 也能调用它们来改变 this 的值。在非严格模式下, this 被强制指向全局对象。而在严格模式下,构造函数会抛出一个错误(由于严格模式下没有为全局对象设置 this,this 保持为 undefined)。
而不少内建构造函数,例如 Array、RegExp 不须要 new 也能正常工做,这是由于它们被设计为做用域安全的构造函数。
当用 new 调用一个函数时,this 指向的新建立的对象已经属于该构造函数所表明的自定义类型。所以,可在函数内用 instanceof 检查本身是否被 new 调用。

function Person(name){
    if(this instanceof Person){
        // called with "new"
    }else{
        // called without "new"
    }
}

具体案例:

function Person(name){
    if(this instanceof Person){
        this.name = name;
    }else{
        return new Person(name);
    }
}
相关文章
相关标签/搜索