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 (<anonymous>)
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 稳妥构造函数模式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容