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方法或者字面量语法中的get和set方法(下面有个例子),定义访问器属性时,get函数和set函数都不是必须的,只定义get意味着属性是只读的,尝试修改属性的操作将无效,会被忽略(严格模式下会抛出错误),同样,只定义set函数意味着该属性不能被读取,如果读取会返回undefined。
1 | // 使用Object.defineProperty定义访问器属性 |
使用Object.defineProperties方法可以同时定义多个属性,第二个参数为所有需要定义的属性组成的对象。
使用delete关键字来删除对象的属性
工厂模式🏭
用函数封装一个对象的创建过程,每次调用工厂函数都会返回一个新对象,这些新对象之间彼此独立,没有共享数据,
构造函数
按照惯例,构造函数函数名首字母要大写,如Person
1 | function Person(name, age) { |
使用new操作符创建一个构造函数的实例会经历如下操作:
- 在内存中创建一个新对象
- 新对象内部的
[[prototype]]特性被赋值为构造函数的prototype属性 - 构造函数内部的this指向这个新对象
- 执行构造函数内部的代码
- 如果构造函数返回了非空对象,则返回该对象;否则返回创建的新对象
构造函数也是函数,它与普通函数唯一的不同就是调用方式。任何函数只要使用new操作符调用就是构造函数,以上面的Person函数为例
1 | // 当做构造函数调用 |
js中绝大部分对象都是通过某个构造函数创建的,可以是内置的构造函数比如Object, Array, Date等,也可以是自定义的构造函数
一些特殊情况是:
- 字面量语法创造的对象本质上是通过Object 构造函数;
- Object.create(null) 创造的对象没有构造函数,其__proto__属性为undefined
原型模式
每个函数都会创建一个prototype属性。prototype属性是一个对象,包含有特定引用类型的实例共享的属性和方法。
使用原型的好处是:在它上面定义的属性和方法可以被对象实例共享,
理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象),默认情况下,所有原型对象自动获得一个constructor属性,指回与之关联的构造函数,以上面的Person函数为例,Person.prototype.constructor指向Person函数。
关键理解:实例与构造函数原型之间有直接的联系,但实例与构造函数本身没有
1 | // 这是构造函数 |
当使用构造函数创建一个新实例,这个实例的[[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 | /* |
【注意】:constructor属性只存在于原型对象,p.contructor是通过原型链找到的,实际上是p.__proto__.constructor,hasOwnProperty方法可以判断访问的属性来自实例还是原型。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 | function Father() { |
以字面量修改原型对象的方式会破坏父类和子类之间的原型链关系,还会丢失constructor的引用
1 | function Father() {} |
==原型上的搜索过程是动态的==,按照上面说的构造函数执行过程,实例在被创建时,其__proto__会自动赋值为当前构造函数的原型对象,后续任何对原型对象属性和方法的修改都能够立即反映在实例上。
但对整个原型对象重新赋值后,以前创建的实例的__proto__还是指向最初的原型对象。
举两个例子来说:
- 先创建实例,再修改构造函数的prototype属性,在实例上访问prototype对象的方法依然有效
1
2
3
4
5function Person () {}
const p = new Person();
Person.prototype.sayHi = function() { console.log('hi'); }
}
p.sayHi(); // 'hi' - 但如果直接对构造函数的prototype重新赋值,实例中的
__proto__([[Protytype]]特性),依然指向调用构造函数生成该实例时的prorotype对象,而非重新赋值后的值1
2
3
4
5
6
7
8
9
10function 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 | Person; // 构造函数 |
我们可以回答下面几个问题:
什么具有原型对象?
答:函数对象具有原型对象,也就是具有prototype属性,而普通对象没有prototype属性,但普通对象具有[[Prototype]]特性,通常通过__proto__属性访问。
原型链上的是什么
答:是原型对象,当一个对象o的__proto__属性指向的对象(即o的原型)也是某个构造函数的实例,就构成了原型链,