前置知识
对象属性的特性
JavaScript权威指南(原书第6版)
对象除了名字和值之外,每个属性还有一些与之相关的值,称为“属性特性”
- 可写(writable attribute),表明是否可以设置该属性的值。
- 枚举(enumerable attribute),表明是否可以通过for/in循环返回该属性。
- 可配置(configurable attribute),表明是否可以删除或修改该属性。
在ECMAScript 5之前,通过代码给对象创建的所有属性都是可写的、可枚举的和可配置的。在ECMAScript 5中则可以对这些特性加以配置。6.7节讲述如何操作。
数据属性:
- value 值
- writable 可写入
- enumerable 可枚举
- configurable 可配置
对象特性
除了包含属性之外,每个对象还拥有三个相关的对象特性(object attribute)
- 对象的原型(prototype)指向另外一个对象,本对象的属性继承自它的原型对象。
- 每个JavaScript对象都有一个原型属性,指向另一个对象。这个原型对象可以包含属性和方法。
- 当你访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript会查找它的原型对象,直到找到该属性或到达原型链的顶端(通常是 Object.prototype)。
- 通过这种机制,对象可以继承其他对象的属性和方法,实现代码重用。
- 对象的类(class)是一个标识对象类型的字符串。
- 对象的扩展标记(extensible flag)指明了(在ECMAScript 5中)是否可以向该对象添加新属性。
- 这个标记指示一个对象是否可以添加新属性。在ECMAScript 5中,可以通过 Object.isExtensible(obj) 方法来检查一个对象是否可扩展。
- 默认情况下,所有对象都是可扩展的,但可以通过 Object.preventExtensions(obj) 方法将对象标记为不可扩展,一旦对象被标记为不可扩展,就不能再向其添加新属性。
Object.isExtensible({}) // true
Object.isExtensible(Object.seal({})) // false
Object.isExtensible(Object.freeze({})) // false
Object.isExtensible({}) // true
Object.isExtensible(Object.seal({})) // false
Object.isExtensible(Object.freeze({})) // false
getter / settter
Note
我们知道,对象属性是由名字、值和一组特性(attribute)构成的。在ECMAScript 5中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter。由getter和setter定义的属性称做“存取器属性”(accessor property),它不同于“数据属性”(data property),数据属性只有一个简单的值。
- 当程序查询存取器属性的值时,JavaScript调用getter方法(无参数)
- 设置一个存取器属性的值时,JavaScript调用setter方法,将赋值表达式右侧的值当做参数传入setter
let obj = {
_value: 0,
get() {
return this._value
},
set(val) {
if (typeof val !== 'number') {
throw new Error('value must be a number')
}
return this._value = val;
}
}
console.log(obj.get())
obj.set(1)
console.log(obj.get())
obj.set("1") // Uncaught Error: value must be a number
let obj = {
_value: 0,
get() {
return this._value
},
set(val) {
if (typeof val !== 'number') {
throw new Error('value must be a number')
}
return this._value = val;
}
}
console.log(obj.get())
obj.set(1)
console.log(obj.get())
obj.set("1") // Uncaught Error: value must be a number
Object.defineProperty
Object.definePeoperty()
用于设置属性的特性、或者让新建属性具有某种特性。
入参:
- 目标对象
- 创建或修改的属性名称
- 属性描述符对象
/*
// 对于新创建的属性来说,默认的特性值是false或undefined
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}*/
let obj = {};
Object.defineProperty(obj, 'name', {
value: 'linghuchong',
configurable: true,
enumerable: false, // 影响 JSON.stringify 。 false的找不到
writable: false
})
console.log(Object.keys(obj)) // 因为设置了enumerable=false,这里是 []
console.log(JSON.stringify(obj)) // 因为设置了enumerable=false,这里是 {}
console.log(obj.name) // linghuchong
obj.name = 'b' // typeError: Cannot assign to read only property 'name' of object
/*
// 对于新创建的属性来说,默认的特性值是false或undefined
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}*/
let obj = {};
Object.defineProperty(obj, 'name', {
value: 'linghuchong',
configurable: true,
enumerable: false, // 影响 JSON.stringify 。 false的找不到
writable: false
})
console.log(Object.keys(obj)) // 因为设置了enumerable=false,这里是 []
console.log(JSON.stringify(obj)) // 因为设置了enumerable=false,这里是 {}
console.log(obj.name) // linghuchong
obj.name = 'b' // typeError: Cannot assign to read only property 'name' of object
需要注意:get / set 和 value / writable 是相互独立的属性,不能同时出现在同一个属性定义中。
Object.getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor() 是 JavaScript 中的一个内置方法,用于获取对象自身某个属性的描述符。这个方法可以帮助我们了解一个属性的详细信息,比如它是否可枚举、是否可写、是否可配置,以及它的值等。
语法
Object.getOwnPropertyDescriptor(obj, prop)
• obj: 目标对象,即我们想要查询属性的对象。
• prop: 字符串,表示我们想要获取描述符的属性名。
返回值
该方法返回一个对象,包含以下属性:
- value: 属性的值。
- writable: 一个布尔值,表示属性是否可以被赋值(即是否可写)。
- enumerable: 一个布尔值,表示属性是否可以在 for...in 循环中被枚举。
- configurable: 一个布尔值,表示属性是否可以被删除,或其特性是否可以被修改。
如果指定的属性不存在,返回 undefined。
示例
const obj = {
name: 'Alice',
age: 25
};
// 获取 'name' 属性的描述符
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
/*
输出:
{
value: 'Alice',
writable: true,
enumerable: true,
configurable: true
}
*/
const obj = {
name: 'Alice',
age: 25
};
// 获取 'name' 属性的描述符
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
/*
输出:
{
value: 'Alice',
writable: true,
enumerable: true,
configurable: true
}
*/
Object.seal
Object.seal()方法封闭一个对象,具有以下效果:
• 阻止添加新属性
• 将现有属性标记为不可配置(configurable:false) 不可删除属性
• 但允许修改现有属性的值
代码示例:
let obj = { a: 1, b: 2 };
Object.seal(obj);
obj.a = 3; // 可修改现有属性的值
delete obj.b; // 无法删除属性
obj.c = 4; // 无法新增属性
let obj = { a: 1, b: 2 };
Object.seal(obj);
obj.a = 3; // 可修改现有属性的值
delete obj.b; // 无法删除属性
obj.c = 4; // 无法新增属性
Object.preventExtensions
防止对象被扩展,也就是说,不能向该对象添加新的属性,但是支持修改或删除。
const obj = { name: "Alice" };
Object.preventExtensions(obj);
obj.age = 25; // 这个操作将会失败,age 属性不会被添加
console.log(obj); // 输出: { name: "Alice" }
obj.name = "Bob"; // 这个操作是允许的,修改现有属性
console.log(obj); // 输出: { name: "Bob" }
delete obj.name; // 这个操作也是允许的,可以删除现有属性
console.log(obj); // 输出: {}
console.log(Object.isExtensible(obj)); // 输出: false
const obj = { name: "Alice" };
Object.preventExtensions(obj);
obj.age = 25; // 这个操作将会失败,age 属性不会被添加
console.log(obj); // 输出: { name: "Alice" }
obj.name = "Bob"; // 这个操作是允许的,修改现有属性
console.log(obj); // 输出: { name: "Bob" }
delete obj.name; // 这个操作也是允许的,可以删除现有属性
console.log(obj); // 输出: {}
console.log(Object.isExtensible(obj)); // 输出: false
Object.propertyIsEnumerable
• Object.propertyIsEnumerable() 方法可以用来判断一个对象的指定属性是否可枚举。
• 对于字符串类型的属性, Object.propertyIsEnumerable() 会返回 true, 表示该字符串属性是可枚举的。
• 可枚举的属性可以通过 for...in 循环遍历到, 但不包括原型链上的属性和 Symbol 命名的属性。 ??
// 字符串属性
const obj = {
name: 'Alice',
age: 25
};
console.log(obj.propertyIsEnumerable('name')); // true
console.log(obj.propertyIsEnumerable('age')); // true
// 原型链上的属性
Object.prototype.hobby = 'reading';
console.log(obj.propertyIsEnumerable('hobby')); // false
// Symbol 命名的属性
const sym = Symbol('id');
obj[sym] = 1234;
console.log(obj.propertyIsEnumerable(sym)); // true
// 字符串属性
const obj = {
name: 'Alice',
age: 25
};
console.log(obj.propertyIsEnumerable('name')); // true
console.log(obj.propertyIsEnumerable('age')); // true
// 原型链上的属性
Object.prototype.hobby = 'reading';
console.log(obj.propertyIsEnumerable('hobby')); // false
// Symbol 命名的属性
const sym = Symbol('id');
obj[sym] = 1234;
console.log(obj.propertyIsEnumerable(sym)); // true
Object.hasOwnProperty
对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回false:
const obj = {
name: 'Alice',
age: 25
};
console.log(obj.hasOwnProperty('name')); // 输出: true
console.log(obj.hasOwnProperty('age')); // 输出: true
console.log(obj.hasOwnProperty('gender')); // 输出: false
const obj = {
name: 'Alice',
age: 25
};
console.log(obj.hasOwnProperty('name')); // 输出: true
console.log(obj.hasOwnProperty('age')); // 输出: true
console.log(obj.hasOwnProperty('gender')); // 输出: false
vue3为什么改用了Proxy
[参考](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/90)
- Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
- Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
使用Object.defineProperty监听
赋值时打印log
function defineReactive(obj, key) {
let val = obj[key];
Object.defineProperty(obj, key, {
enumerable: true, configurable: true, get: function () {
return val
}, set: function (newVal) {
if (val === newVal) {
return
}
console.log(`${val} updated to ${newVal}`)
val = newVal
}
})
}
function observerObject(obj) {
const keys = Object.keys(obj);
for (const key of keys) {
defineReactive(obj, key)
}
}
let obj = {
a: 20, b: 2, c: 3,
}
observerObject(obj)
obj.a = 21 // 20 updated to 21
obj.b = 24 // 2 updated to 24
obj.f = 'panda' // 无log
function defineReactive(obj, key) {
let val = obj[key];
Object.defineProperty(obj, key, {
enumerable: true, configurable: true, get: function () {
return val
}, set: function (newVal) {
if (val === newVal) {
return
}
console.log(`${val} updated to ${newVal}`)
val = newVal
}
})
}
function observerObject(obj) {
const keys = Object.keys(obj);
for (const key of keys) {
defineReactive(obj, key)
}
}
let obj = {
a: 20, b: 2, c: 3,
}
observerObject(obj)
obj.a = 21 // 20 updated to 21
obj.b = 24 // 2 updated to 24
obj.f = 'panda' // 无log
新增属性时无log,因为没有对新增加的属性添加监听,下面增加一个$set
方法,监听新增的属性:
obj.$set = function (key, value) {
this[key] = value
defineReactive(this, key)
}
obj.$set('f', 'cat')
obj.f = 'dog' // cat updated to dog
obj.$set = function (key, value) {
this[key] = value
defineReactive(this, key)
}
obj.$set('f', 'cat')
obj.f = 'dog' // cat updated to dog
动态监听属性
function defineReactive(obj, key) {
let val = obj[key];
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true, configurable: true, get: function () {
dep.depend()
return val
}, set: function (newVal) {
if (val === newVal) {
return
}
// console.log(`${val} updated to ${newVal}`)
val = newVal
dep.notify(newVal, val)
}
})
}
class Dep {
constructor() {
// 需要支持多个watcher
this.subs = []
}
depend() {
Dep.target && this.subs.push(Dep.target /*watcher*/)
}
notify(newVal, oldVal) {
for (let i = 0; i < this.subs.length; i++) {
const watcher = this.subs[i]
watcher.fn.call(watcher.vm, newVal, oldVal)
}
}
}
function observerObject(obj) {
const keys = Object.keys(obj);
for (const key of keys) {
defineReactive(obj, key)
}
}
Dep.target = null
class Watch {
constructor(data, prop, fn) {
this.vm = data
this.prop = prop
this.fn = fn
Dep.target = this
// 用于触发getter,将watcher添加到Dep中
data[prop]
Dep.target = null
}
}
let obj = {
a: 20, b: 2, c: 3,
}
observerObject(obj)
// 注意这里的func不要使用箭头函数,否则获取不到this
new Watch(obj, 'a', function (val, oldVal) {
console.log('update a changed1:', val, oldVal, this)
})
new Watch(obj, 'a', function (val, oldVal) {
console.log('update a changed2:', val, oldVal, this)
})
new Watch(obj, 'b', function (val, oldVal) {
console.log('update b changed:', val, oldVal, this)
})
function defineReactive(obj, key) {
let val = obj[key];
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true, configurable: true, get: function () {
dep.depend()
return val
}, set: function (newVal) {
if (val === newVal) {
return
}
// console.log(`${val} updated to ${newVal}`)
val = newVal
dep.notify(newVal, val)
}
})
}
class Dep {
constructor() {
// 需要支持多个watcher
this.subs = []
}
depend() {
Dep.target && this.subs.push(Dep.target /*watcher*/)
}
notify(newVal, oldVal) {
for (let i = 0; i < this.subs.length; i++) {
const watcher = this.subs[i]
watcher.fn.call(watcher.vm, newVal, oldVal)
}
}
}
function observerObject(obj) {
const keys = Object.keys(obj);
for (const key of keys) {
defineReactive(obj, key)
}
}
Dep.target = null
class Watch {
constructor(data, prop, fn) {
this.vm = data
this.prop = prop
this.fn = fn
Dep.target = this
// 用于触发getter,将watcher添加到Dep中
data[prop]
Dep.target = null
}
}
let obj = {
a: 20, b: 2, c: 3,
}
observerObject(obj)
// 注意这里的func不要使用箭头函数,否则获取不到this
new Watch(obj, 'a', function (val, oldVal) {
console.log('update a changed1:', val, oldVal, this)
})
new Watch(obj, 'a', function (val, oldVal) {
console.log('update a changed2:', val, oldVal, this)
})
new Watch(obj, 'b', function (val, oldVal) {
console.log('update b changed:', val, oldVal, this)
})
- 需要遍历object所有自有属性
- 全局定义了一个
Dep.target
,用于getter/setter中获取Dep.target
- Dep中的
subs
是一个数组,因为可能有n个Watch监听object的某个字段 - 如果是object嵌套,需要再循环调用一下,代码略
如何监听数组属性
Vue2中的做法是对Array进行了扩展:
defineReactive方法
defineReactive
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 获取属性描述符,只操作可配置属性
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取getter / setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 如果原本有getter,调用getter方法。否则取val
const value = getter ? getter.call(obj) : val
// 使对象后续有可被监听的能力
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 如果原本有getter,调用getter方法。否则取val
const value = getter ? getter.call(obj) : val
/**
* newVal !== newVal && value !== value 这里看着很奇怪,但是想到 NaN !== NaN 就恍然了
*/
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env. NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 因为getter/setter和value不是同时存在的。只有getter也没必要执行后面的操作了
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 获取属性描述符,只操作可配置属性
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取getter / setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 如果原本有getter,调用getter方法。否则取val
const value = getter ? getter.call(obj) : val
// 使对象后续有可被监听的能力
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 如果原本有getter,调用getter方法。否则取val
const value = getter ? getter.call(obj) : val
/**
* newVal !== newVal && value !== value 这里看着很奇怪,但是想到 NaN !== NaN 就恍然了
*/
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env. NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 因为getter/setter和value不是同时存在的。只有getter也没必要执行后面的操作了
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
接下来看Dep
Dep
depend
dep.depend
: 将Dep.target/Watcher
添加到subs中
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
...
depend () {
if (Dep.target) {
Dep.target.addDep(this) // 1
}
}
addSub (sub: Watcher) { // 4
this.subs.push(sub)
}
}
export default class Watcher {
depIds: SimpleSet;
newDeps: Array<Dep>;
newDepIds: SimpleSet;
....
addDep (dep: Dep) { // 2
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this) // 3
}
}
}
...
update () { // b
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
...
depend () {
if (Dep.target) {
Dep.target.addDep(this) // 1
}
}
addSub (sub: Watcher) { // 4
this.subs.push(sub)
}
}
export default class Watcher {
depIds: SimpleSet;
newDeps: Array<Dep>;
newDepIds: SimpleSet;
....
addDep (dep: Dep) { // 2
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this) // 3
}
}
}
...
update () { // b
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
notify
export default class Dep {
static target: ?Watcher;
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env. NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct // order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // a
}
}
}
export default class Watcher {
depIds: SimpleSet;
newDeps: Array<Dep>;
newDepIds: SimpleSet;
....
update () { // b
/* istanbul ignore else */
if (this.lazy) {
// 如果 watcher 是懒加载模式(lazy),
// 它只需将 dirty 标志设置为 true。
// 这意味着当下次访问这个值时,它会触发重新计算
this.dirty = true
} else if (this.sync) {
// 同步模式例如 <xx :dd.sync="value">
this.run()
} else {
queueWatcher(this)
}
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately. let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env. NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
export default class Dep {
static target: ?Watcher;
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env. NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct // order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // a
}
}
}
export default class Watcher {
depIds: SimpleSet;
newDeps: Array<Dep>;
newDepIds: SimpleSet;
....
update () { // b
/* istanbul ignore else */
if (this.lazy) {
// 如果 watcher 是懒加载模式(lazy),
// 它只需将 dirty 标志设置为 true。
// 这意味着当下次访问这个值时,它会触发重新计算
this.dirty = true
} else if (this.sync) {
// 同步模式例如 <xx :dd.sync="value">
this.run()
} else {
queueWatcher(this)
}
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately. let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env. NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}