banner
NEWS LETTER

I Know JS 对象篇

Scroll down

object

有一些内部特性用来描述属性特征,不能直接访问这些特性,用两个中括号[[]]把这些特性括起来,例如[[Enumerable]]

Object的属性可以分为两类:数据属性访问器属性
数据属性包含一个保存数据值的位置,有4个特性来描述:

  • [[Configurable]]:表示属性是否可以被delete删除、是否可以修改此特性、是否可以属性改为数据属性。默认值true
  • [[Enumerable]]:表示属性是否可以通过for-in属性循环,默认值true
  • [[Writable]]:表示属性的值是否可以被修改,默认值true
  • [[Value]]:包含实际属性的值,默认值undefined

访问器属性不包含数据值,包含一个getter函数和setter函数(都不是必需的),在读取访问器属性时,会调用getter函数;在写入访问器属性时,会调用setter函数。访问器属性有4个特性来描述:

  • [[Configurable]]:表示属性是否可以被delete删除、是否可以修改此特性、是否可以属性改为数据属性。默认值true
  • [[Enumerable]]:表示属性是否可以通过for-in属性循环,默认true
  • [[Get]]:获取函数,读取属性时调用,默认值undefined
  • [[Set]]:设置函数,在写入属性时调用,默认值undefined

当我们使用字面量语法定义一个属性时,该属性是一个数据属性,且上述4个特性都是默认值,如果要定义访问器属性,必须使用Object.defineProperty方法或者字面量语法中的getset方法(下面有个例子),定义访问器属性时,get函数和set函数都不是必须的,只定义get意味着属性是只读的,尝试修改属性的操作将无效,会被忽略(严格模式下会抛出错误),同样,只定义set函数意味着该属性不能被读取,如果读取会返回undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用Object.defineProperty定义访问器属性
Object.defineProperty(obj, 'name', {
get() {
return 'bob';
},
set(newV) {
console.log('was setted to ', newV);
},
configurable: true,
enumerable: true
})
// 使用字面量语法定义访问器属性
const obj = {
get name() {
return 'bob';
},
set name(newV) {
console.log(newV)
}
}

使用Object.defineProperties方法可以同时定义多个属性,第二个参数为所有需要定义的属性组成的对象。
使用delete关键字来删除对象的属性

工厂模式🏭

用函数封装一个对象的创建过程,每次调用工厂函数都会返回一个新对象,这些新对象之间彼此独立,没有共享数据,

构造函数

按照惯例,构造函数函数名首字母要大写,如Person

1
2
3
4
5
6
7
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name)
}
}

使用new操作符创建一个构造函数的实例会经历如下操作:

  1. 在内存中创建一个新对象
  2. 新对象内部的[[prototype]]特性被赋值为构造函数的prototype属性
  3. 构造函数内部的this指向这个新对象
  4. 执行构造函数内部的代码
  5. 如果构造函数返回了非空对象,则返回该对象;否则返回创建的新对象

构造函数也是函数,它与普通函数唯一的不同就是调用方式。任何函数只要使用new操作符调用就是构造函数,以上面的Person函数为例

1
2
3
4
5
6
7
// 当做构造函数调用
let p1 = new Person('Alice', 22);
p1.sayName(); // 'Alice'

// 当做普通函数调用
Person('Bob', 23);
window.name; // 'Bob'

js中绝大部分对象都是通过某个构造函数创建的,可以是内置的构造函数比如Object, Array, Date等,也可以是自定义的构造函数
一些特殊情况是:

  • 字面量语法创造的对象本质上是通过Object 构造函数;
  • Object.create(null) 创造的对象没有构造函数,其__proto__属性为undefined

原型模式

每个函数都会创建一个prototype属性。prototype属性是一个对象,包含有特定引用类型的实例共享的属性和方法。
使用原型的好处是:在它上面定义的属性和方法可以被对象实例共享,

理解原型

无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象),默认情况下,所有原型对象自动获得一个constructor属性,指回与之关联的构造函数,以上面的Person函数为例,Person.prototype.constructor指向Person函数。

关键理解:实例与构造函数原型之间有直接的联系,但实例与构造函数本身没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这是构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
}
}

// 构造函数会有一个原型对象(当然所有函数都有),通过prototype属性可以访问这个原型对象,原型对象有一个constructor属性,指向构造函数本身,构成一个循环引用
// 自定义构造函数时,原型对象默认只有constructor属性,其他方法都继承自Object
console.log(Person.prototype);
/*
{
constructor: f Person(name, age);
}
*/
console.log(Person.prototype.constructor === Person); // true

当使用构造函数创建一个新实例,这个实例的[[Prototype]]特性就会被赋值为构造函数的原型对象,Firefox、Safari、Chrome会在每个对象上暴露__proto__属性,通过此属性可以访问[[Prototype]]特性,即:对象o的__proto__属性指向该对象o的构造函数的原型对象。
对一个对象o来说,通过__proto__属性可以访问o的构造函数的原型对象,这个原型对象也是一个对象,也有__proto__属性,同样指向它的构造函数的原型对象,这些原型对象之间构成了一个原型链,原型链终止于Object的原型对象。

Object原型的原型是Null

1
Object.prototype.__proto__ === null

使用Object.create()可以创造一个指定原型的对象,传参为要指定的原型对象(__proto__属性)

1
2
3
4
5
6
7
/* 
* Person.prototype也是一个对象,按照上面👆🏻的逻辑,对象的__proto__属性指向该
* 对象构造函数的原型对象,Person.prototype对象的构造函数是Object,所以
* Person.prototype.__proto__属性指向Object的原型。所以下面的表达式为true
*/
Person.prototype.__proto__ === Object.prototype; //true
Person.prototype.__proto__.constructor === Object; // true

【注意】:constructor属性只存在于原型对象,p.contructor是通过原型链找到的,实际上是p.__proto__.constructorhasOwnProperty方法可以判断访问的属性来自实例还是原型。Object.getOwnPropertyDescriptor()方法只对实例属性有效,对原型属性无效。
in操作符用于判断是否可通过对象访问到指定属性,无论指定属性是实例属性还是原型属性。

!obj.hasOwnProperty(prop) && prop in obj 此表达式可用于判断prop属性是否是obj的原型的属性

in 操作符访问实例上所有可访问的属性,不论该属性在实例还是在原型对象上
hasOwnProperty 访问某个属性是否存在于实例上
Object.keys 列举出可枚举的属性值
Object.getOwnPropertyNames 列举出所有可枚举和不可枚举的属性
Object.getOwnPropertySymbols 列举出符号键属性

for-in 循环枚举和Object.keys 对属性的枚举其顺序是不确定的,因浏览器JS引擎而异
Object.getOwnPropertyNames、getOwnPropertySymbols Object.assign 对属性的枚举顺序是确定的:先以升序枚举数值键,然后以插入顺序枚举字符串和符号键

==原型链和继承==
子类可以使用父类的方法,也可以覆盖或新增父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Father() {
this.role = 'father'
}
Father.prototype.introduce = function () {
console.log('i am father')
}
function Son() {
this.role = 'son';
}
// 将子类构造函数的原型对象设置为父类的实例,这样可以构成原型链继承关系
Son.protorype = new Father();

// 覆盖父类的方法,子类实例在查找时会先找到自己这个
Son.prototype.introduce = function() {
console.log('i am son');
}
// 新增子类方法
Son.protorype.cry = function () {
console.log('start cry')
}

以字面量修改原型对象的方式会破坏父类和子类之间的原型链关系,还会丢失constructor的引用

1
2
3
4
5
6
7
8
function Father() {}
Father.prototype.eat = function () { console.log('eat two')}
function Son() {}
Son.prototype = new Father()
// 会覆盖Son的原型对象,变成一个Object的实例,
Son.prototype = {
eat() { console.log('eat one')}
}

==原型上的搜索过程是动态的==,按照上面说的构造函数执行过程,实例在被创建时,其__proto__会自动赋值为当前构造函数的原型对象,后续任何对原型对象属性和方法的修改都能够立即反映在实例上。
但对整个原型对象重新赋值后,以前创建的实例的__proto__还是指向最初的原型对象。
举两个例子来说:

  1. 先创建实例,再修改构造函数的prototype属性,在实例上访问prototype对象的方法依然有效
    1
    2
    3
    4
    5
    function Person () {}
    const p = new Person();
    Person.prototype.sayHi = function() { console.log('hi'); }
    }
    p.sayHi(); // 'hi'
  2. 但如果直接对构造函数的prototype重新赋值,实例中的__proto__([[Protytype]]特性),依然指向调用构造函数生成该实例时的prorotype对象,而非重新赋值后的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person () {}
    const p = new Person();
    Person.prototype = {
    sayHi() {console.log('hi');}
    }
    p.sayHi(); // Uncaught TypeError: p.sayHi is not a function
    // 因为最初的原型上没有sayHi方法
    // 修改原型对象后创建一个新的实例则可以
    const p2 = new Person();
    p2.sayHi(); // 'hi'

总结:

1
2
3
4
5
Person; // 构造函数
let p = Person(); // 用构造函数创建的新实例
p.constructor === Person; // true
Person.prototype; // 指向构造函数的原型对象
Person.prototype.constructor; // 构造函数原型对象的constructor属性指向构造函数

我们可以回答下面几个问题:
什么具有原型对象?
答:函数对象具有原型对象,也就是具有prototype属性,而普通对象没有prototype属性,但普通对象具有[[Prototype]]特性,通常通过__proto__属性访问。

原型链上的是什么
答:是原型对象,当一个对象o的__proto__属性指向的对象(即o的原型)也是某个构造函数的实例,就构成了原型链,

其他文章