js 对象的继承

一、理解 js 对象

1.1 创建对象

法一:

var person = new Object()
person.name = 'zhangsan'
person.age = 25
person.job = 'worker'
person.sayName = function() {
  alert(this.name)
}

法二:

var person = {
  name: 'lisi',
  age: '20',
  job: 'lawyer',
  sayName: function() {
    alert(this.name)
  }
}

1.2 属性(property)类型

1.2.1 数据属性

四个特性:

[[ Configurable ]]:表示能否通过 delete 进行删除属性的特性,默认 true
[[ Enumerable ]]: 表示能否通过 for-in 循环返回属性, 默认 true
[[ Writable ]]:表示能否修改属性的值,默认 true
[[ Value ]]: 包含这个属性的数据值,读取属性的时候,从这个位置读;写入属性的时候,把新值保存到这个位置,默认值 undefined
注意:以上四个特性都是针对像前面例子中那样 直接在对象上定义的属性

例如:

var person = {
  name: '张三'
}
console.log(person) // {name: '张三'}
delete person.name // true    [[ Configurable ]]特性
console.log(person) // {}
person.name = '李四'  // [[ Writable ]]特性
console.log(person) // {name: "lisi"}

那么这里创建了一个 name 属性,那么[[ Configurable ]] [[ Enumerable ]] [[ Writable ]]这三个特性默认为true,[[ Value ]]特性被设置为 张三
问题:怎么修改属性默认的特性呢?使用 Object.defineProperty() 方法
该方法接受三个参数:属性所在的对象,属性的名字和一个描述符对象
示例:

var person = {
  name: 'lisi'
}
Object.defineProperty(person, 'name', {
    writable: false,
    configurable: false
})
console.log(person) // {name: 'lisi'}
delete person.name // false
console.log(person) // {name: 'lisi'}
person.name = '王五'
console.log(person) // {name: 'lisi'}

注意:

  1. 使用 Object.defineProperty() 方法定义对象的属性时,如果把 configurable 设置为 false ,就是把属性定义为不可配置的,就不能把它变成可配置的了。也就是说,Object.defineProperty() 方法修改同一属性是可以多次调用的,但是把 configurable 设置成 false 就会有限制啦
var animal = {}
Object.defineProperty(animal, 'name', {
  configurable: false,
  value: 'dog'
})
delete animal.name // false
Object.defineProperty(animal, 'name', {
  configurable: true,
  value: 'dog'
}) // 报错 Uncaught TypeError: Cannot redefine property: name at Function.defineProperty ()
var people = {}
Object.defineProperty(people, 'age', {
  writable: true,
  value: '20'
})
people.age = '30'
console.log(people) // {age: "30"}
Object.defineProperty(people, 'age', {
  writable: false,
  value: '20'
})
people.age = '30'
console.log(people) // {age: "20"}
  1. 在调用 Object.defineProperty() 方法定义属性时,如果不指定, configurable, enumerable, writable特性的默认值都是false,注意与直接在对象上定义的属性的这些特性默认为 true 进行区分

1.2.2 访问器属性

定义:访问器属性不能直接定义,必须使用Object.defineProperty()来定义。访问器属性不包含数据值,他们包含一对儿 gettersetter 函数(这两个函数都不是必须的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回(return)有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责如何处理数据
四个特性:

[[ Configurable ]]:表示能否通过 delete 进行删除属性的特性,默认 true
[[ Enumerable ]]: 表示能否通过 for-in 循环返回属性, 默认 true
[[Get]]: 在读取属性时调用的函数,默认值是 undefined
[[Set]]: 在写入属性时调用的函数,默认值是 undefined
使用:常通过设置一个属性的值会导致其他属性发生变化

var book = {
    _year: 2004,
    edition: 1
}
undefined
Object.defineProperty(book, 'year', {
    get: function(){
        return this._year
    },
    set: function(newVal){
        if(newVal > 2004){
            this._year = newVal
            this.edition += newVal - 2004
        }
    }
})
{_year: 2004, edition: 1}
book.year = 2006
2006
book.edition    
3

注意:不一定同时指定getter 和 setter ,只指定getter 意味着属性不能写,只指定 setter 意味着属性不能读。

1.3 定义多个属性

var book = {}
Object.defineProperties(book, {
    _year: {
        writable: true, // 不要忘了
        value: 2004
    },
    edition: {
        writable: true,
        value: 1
    },
    year: {
        get: function(){
            return this._year
        },
        set: function(newVal){
            if(newVal > 2004){
                this._year = newVal
                this.edition += newVal - 2004
            }
        }
    }
})
// 读取
var yyy = Object.getOwnPropertyDescriptor(book, '_year')
yyy.value
2004
yyy.configurable
false
yyy.writable
true

二、创建对象

通常我们使用 Object 构造函数或者对象字面量都可以创建单个对象,但有个明显的缺点,会产生大量的重复代码

2.1 工厂模式

function createPerson(name, age, job) {
    var obj = new Object()
    obj.name = name
    obj.age = age
    obj.job = job
    obj.sayName = function(){
        alert(this.name)
    }
    return obj
}
var person1 = createPerson('xiaoming', 20, 'worker')
var person2 = createPerson('lisi', 18, 'lawyer')
person1
{name: "xiaoming", age: 20, job: "worker", sayName: ƒ}
person2
{name: "lisi", age: 18, job: "lawyer", sayName: ƒ}

优点和不足:

优点:解决了创建多个相似对象代码重复的问题
不足:没有解决对象识别的问题(即怎样知道一个对象的类型),使用工厂模式创建的对象实例都只能判断为 Object 类型,有时候我们想把实例对象标识为一种特定的类型,比如:Person, Animal等,工厂模式做不到

person1 instanceof Object // true
person2 instanceof Object // true
person1 instanceof Person // 报错

2.2 构造函数模式

ECMAScript中的构造函数可用来创建特定类型的对象,比如 Object, Array, Function, String等这些原生构造函数,在运行时会自动出现在执行环境中。此外,我们也可以创建自定义的构造函数,从而自定义对象类型的属性和方法。

function Person(name, age, job){
    this.name = name
    this.age = age
    this.job = job
    this.sayName = function(){
        alert(this.name)
    }
   //this.sayName = new Function('alert(this.name)')
}
var person1 = new Person('zhangsan', 20, 'worker')
person1
Person {name: "zhangsan", age: 20, job: "worker", sayName: ƒ}
var person2 = new Person('xiaoming', 15, 'I do not know')
person2
Person {name: "xiaoming", age: 15, job: "I do not know", sayName: ƒ}

2.2.1 new 操作符的作用

  1. 创建一个新对象(空的)
  2. 将构造函数的作用域赋给新对象(也就是把 this 指向新的实例对象)
  3. 执行构造函数的代码(给新对象添加属性和方法)
  4. 返回新对象

2.2.2 构造函数和普通函数

任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;任何函数,如果不通过 new 操作符来调用,那它跟普通函数不会有什么两样。所以,构造函数和普通函数的唯一区别,就在于调用他们的方式不同。
以上面代码为例:

// 当作构造函数使用
person1.sayName() // 'zhangsan'
// 当作普通函数调用
Person('lisi', 18, 'Doctor') // 函数处在全局作用域下,此时 `this` 是 window
window.sayName() // 'lisi'
// 在另一个对象的作用域中调用
var o = new Object()
Person.call(o, 'wangwu', 30, 'lawyer')
o.sayName() // 'wangwu'

2.2.3 构造函数的优点和不足

优点:减少重复代码,可以将实例标识为一种特定的类型

person1 instanceof Object // true
person1 instanceof Person // true

不足:使用构造函数创建实例对象时,在构造函数定义的每个方法都要在每个新创建的实例上重新创建一遍,每一个函数都是会耗费空间的,浪费空间

person1.sayName == person2.sayName // false

然而,创建两个完成同样任务的 Function 实例方法的确没有必要
所以:

function Person(name, age, job){
    this.name = name
    this.age = age
    this.job = job
    this.sayName = sayName 
}
function sayName () {
    alert(this.name)
}
var person1 = new Person('zhangsan', 20, 'worker')
var person2 = new Person('xiaoming', 15, 'I do not know')
person1.sayName == person2.sayName // true

问题是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们自定义的构造函数就丝毫没有封装性可言啦
所以,要使用 原型模式 解决

2.3 原型模式

每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型创建的所有实例共享的属性和方法
也就是说,prototype 就是通过调用构造函数而创建的那个实例对象的原型对象。
使用原型对象的好处是可以让所有的对象实例共享它所包含的属性和方法。

function Person() {}
Person.prototype // { constructor: f }
Person.prototype.name = 'zhangsan'
Person.prototype.age = 18
Person.prototype.job = 'worker'
Person.prototype.sayName = function(){
    alert(this.name)
}
Person.prototype // {name: "zhangsan", age: 18, job: "worker", sayName: ƒ, constructor: ƒ}
var person1 = new Person()
person1.sayName() // 'zhangsan'
var person2 = new Person()
person1.sayName == person2.sayName // true

2.3.1 理解原型对象

  1. 创建一个函数 Person,就会根据一组特定的规则为该函数创建一个 prototype 属性
  2. prototype 属性是一个指针,指向一个对象(原型对象Person.prototype
  3. 这个对象会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针 Person.prototype.constructor == Person
  4. 我们可以在这个原型对象上扩展一些属性和方法 Person.prototype.name = 'zhangsan'
  5. 通过这个构造函数 Person 创建的实例对象person1, person2, 内部都有一个对脚本不可见的属性 [[prototype]](__proto__),这个属性包含一个指向原型对象的指针。person1.__proto__ == Person.prototype成立,所以person1.__proto__.name == Person.prototype.name成立,而__proto__对脚本不可见,所以 person1.name == Person.prototype.name 是成立的。这也就是实例对象为什么可以直接访问原型对象的原因。

如何获取一个对象的原型

  1. 通过调用创建这个对象的构造函数的 prototype 属性 Person.prototype
  2. 使用 Object.getPrototypeOf(person1) , Object.getPrototypeOf(person1) == Person.prototype是成立的
原型.png

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。当我们访问一个对象的属性或方法时,先从对象本身开始找,找到了就停止查找,在自身找不到,就会在原型中找。无论实例对象添加的这个同名属性值是什么,比如null, undefined, 只要实例对象中有这个同名属性,就不会去原型中找。

function Person() {}
Person.prototype // { constructor: f }
Person.prototype.name = 'zhangsan'
Person.prototype.age = 18
Person.prototype.job = 'worker'
Person.prototype.sayName = function(){
    alert(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.name = 'xiaoming'
alert(person1.name) // 'xiaoming' --来自实例
alert(person2.name) // 'zhangsan' --来自原型
delete person1.name // 删除掉实例对象中的 `name` 属性
alert(person1.name) // 'xiaoming' --来自原型

我们可以通过 hasOwnProperty 方法检测一个属性是否存在于实例中。这个方法是通过原型链继承从 Object 继承来的,只在给定属性在对象实例中时,才会返回true;给定属性在原型中或者不存在时,返回 false.

function Person() {}
Person.prototype // { constructor: f }
Person.prototype.name = 'zhangsan'
Person.prototype.age = 18
Person.prototype.job = 'worker'
Person.prototype.sayName = function(){
    alert(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.hasOwnProperty('name') // false --来自原型
person1.name = 'xiaoming'
person1.hasOwnProperty('name') // true --来自实例
person2.hasOwnProperty('name')  // false --来自原型

2.3.2 原型与 in 操作符

  1. 单独使用 in 操作符

单独使用时,in 操作符会在通过对象实例能够访问到给定属性时返回 true,无论该属性存在于实例中还是原型中。

function Person() {}
Person.prototype // { constructor: f }
Person.prototype.name = 'zhangsan'
Person.prototype.age = 18
Person.prototype.job = 'worker'
Person.prototype.sayName = function(){
    alert(this.name)
}
var person1 = new Person()
var person2 = new Person()

person1.hasOwnProperty('name') // false
'name' in person1 // true

person1.name = 'xiaoming'
person1.name // 'xiaoming' --来自实例
person1.hasOwnProperty('name') // true
'name' in person1 // true

同时使用 hasOwnProperty() 方法和 in 操作符,可以确定该属性是存在于对象实例中,还是存在于原型中

function hasPrototypeProperty(object, prop) {
  if(prop in object){
    return object.hasOwnProperty(prop)
  } else {
    alert('查不到该属性')
  }
}
hasPrototypeProperty(person1, 'name') // false --来自原型
person1.name = 'xiaoming'
hasPrototypeProperty(person1, 'name') // true --来自实例
hasPrototypeProperty(person1, 'dog') // 查不到该属性 --该属性实例和原型中都没有
hasPrototypeProperty(person1, 'toString') // false --来自原型,从 object 继承而来
  1. for-in 循环中使用
    在使用 for-in 循环时,返回的是所有能够通过对象访问的(访问器属性中有get特性)、可枚举的(enumerated为true)属性,无论该属性存在于实例或者原型中
    说明:
  1. 像一些原生构造函数 Object, Array, String等,所构建的原型对象里面的原生方法比如toString(), valueOf(), constructor等都是不可枚举的(enumerated为false),for-in 循环拿不到这些方法属性
  2. 如果在实例中添加同名属性方法,把原型中的同名属性方法屏蔽掉,可以拿到这个属性。比如:在实例中添加一个 toString 方法,通过for-in可以拿到实例中的 toString 方法。
  1. 两个方法
  1. 取得对象上的所有可枚举的实例属性(对象本身的属性方法,不包括原型)Object.keys()
Object.keys(person1) // ["name"]
Object.keys(Person.prototype) // ["name", "age", "job", "sayName"]
  1. 取得对象上的所有实例属性,无论是否可以枚举 Object.getOwnPropertyNames()
Object.getOwnPropertyNames(person1) // ["name"]
Object.getOwnPropertyNames(Person.prototype) // ["constructor", "name", "age", "job", "sayName"]

2.3.3 更简单的原型语法

function Person() {}
Person.prototype // { constructor: f }
Person.prototype = {
  constructor: Person,
  name: 'zhangsan',
  age: 18,
  job: 'worker',
  sayName: function(){
    alert(this.name)
  }
}

问题:

  1. 每创建一个函数,就会同时创建它的 prototype 对象,这个对象会自动获得 constructor 属性,这个属性包含指向 prototype 属性所在函数的指针。这个对象就是初始的原型对象。
  2. 上面代码本质上重写了默认的 prototype 对象,此时的 Person.prototype 对象只相当于 Object 构造函数的一个实例对象,这个对象本身没有了 constructor 属性,但这个对象可以调用它的原型 Object.prototype 里面的 constructor 属性,指向 Object 构造函数。
  3. 所以,我们重写 prototype 对象时,可以加上 constructor 属性,写上它的正确指向。
  4. 直接在 prototype 对象里写上 constructor 属性会导致它的 [[Enumerable]] 属性被设置成 true,默认情况下,原生的 constructor 属性是不可枚举的。所以我们可以使用 Object.defineProperty() 方法进行定义
Object.defineProperty(Person.prototype, 'constructor', {
 enumerable: false,
 value: Person
})

2.3.4 原型的动态性

var friend = new Person()
Person.prototype.sayHi = function(){
    alert('Hi')
}
friend.sayHi() // 'Hi'

说明:以上我们发现 friend 实例是在添加新方法之前创建的,但仍然可以调用新方法。当我们调用 friend.sayHi() 时,先在实例中找,找不到就继续搜索原型,因为实例与原型之间的连接只不过是一个 [[prototype]] 指针,而不是一个副本。所以我们在原型上做的任何修改都能立即从实例上反映出来。

function Person() {}
var friend = new Person()
Person.prototype.dog = '123'
Person.prototype = {
  constructor: Person,
  name: 'zhangsan',
  age: 18,
  job: 'worker',
  sayName: function(){
    alert(this.name)
  }
}
friend.sayName() // error
friend.dog // dog

说明:尽管可以随时为原型添加属性和方法,并且修改能够在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样啦。我们知道,调用构造函数时会为实例添加一个指向最初原型的 [[prototype]] 指针,只会指向最初的原型。
重写原型只是会切断构造函数与最初原型之间的联系,并不能改变实例对象指向最初原型。
记住:实例中的指针仅指向原型,不指向构造函数。

image.png

2.3.5 原生对象的原型

2.4 构造函数模式与原型模式结合

2.5 动态原型模式

2.6 寄生构造函数模式

2.7 稳妥构造函数模式

你可能感兴趣的:(js 对象的继承)