JavaScript深入02

相信我们都学过vue实现响应式的原理是对象属性上set和get的调用,这其中其实有很多问题,我们在JS中默认的属性值的设置和获取是怎么获取的?为什么有时候返回undefined?设置属性描述符为writable为false后为什么重写会直接失败?下面我们来深入探讨下

1.[[Get]]

属性访问在实现时有一个微妙却非常重要的细节,思考下面的代码:

var myObject = { 
a: 2 
}; 
myObject.a; // 2

myObject.a 是一次属性访问,但是这条语句并不仅仅是在myObjet中查找名字为a的属性, 虽然看起来好像是这样。

在语言规范中,myObject.a在myObject上实际上是实现了[[Get]]操作(有点像函数调 用:[[Get]]())。对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。

然而,如果没有找到名称相同的属性,按照[[Get]]算法的定义会执行另外一种非常重要的行为(其实就是遍历可能存在的[[Prototype]]链, 也就是原型链)

如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回值undefined:

var myObject = {  
a:2 
}; 
myObject.b; // undefined

 注意,这种方法和访问变量时是不一样的。如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回undefined,而是会抛出一个ReferenceError异常:

var myObject = {  
a: undefined 
}; 
myObject.a; // undefined  
myObject.b; // undefined

 从返回值的角度来说,这两个引用没有区别——它们都返回了undefined。然而,尽管乍看之下没什么区别,实际上底层的[[Get]]操作对myObject.b进行了更复杂的处理。 由于仅根据返回值无法判断出到底变量的值为undefined还是变量不存在,所以[[Get]] 操作返回了undefined。不过稍后我们会介绍如何区分这两种情况。

2.[[Put]]

既然有可以获取属性值的[[Get]]操作,就一定有对应的[[Put]]操作。 你可能会认为给对象的属性赋值会触发[[Put]]来设置或者创建这个属性。但是实际情况并不完全是这样。

[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。

如果已经存在这个属性,[[Put]]算法大致会检查下面这些内容。

1. 属性是否是访问描述符(下一节会给出这个词的解释)?如果是并且存在setter就调用setter。

2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在 严格模式下抛出TypeError异常。

3. 如果都不是,将该值设置为属性的值。 如果对象中不存在这个属性,[[Put]]操作会更加复杂。

3.Getter和Setter

对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。

在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法 应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。

当你给一个属性定义getter、setter或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript会忽略它们的value和 writable 特性,取而代之的是关心set和get(还有configurable和enumerable)特性。

思考下面的代码:

var myObject = { 
    // 给a定义一个getter  
    get a() { 
    return 2;  
} 
}; 
Object.defineProperty( 
myObject,  // 目标对象 
"b",       // 属性名 
    {           // 描述符 
        // 给b设置一个getter 
        get: function(){ return this.a * 2 }, 
 
        // 确保b会出现在对象的属性列表中 
        enumerable: true  
    } 
); 
 
myObject.a; // 2 
 
myObject.b; // 4

 不管是对象文字语法中的get a() { .. },还是defineProperty(..)中的显式定义,二者 都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数, 它的返回值会被当作属性访问的返回值:

var myObject = { 
    // 给 a 定义一个getter 
    get a() { 
        return 2;  
    } 
}; 
 
myObject.a = 3; 
 
myObject.a; // 2

由于我们只定义了a的getter,所以对a的值进行设置时set操作会忽略赋值操作,不会抛出错误。而且即便有合法的setter,由于我们自定义的getter只会返回2,所以set操作是没有意义的。

为了让属性更合理,还应当定义setter,和你期望的一样,setter会覆盖单个属性默认的 [[Put]](也被称为赋值)操作。通常来说getter和setter是成对出现的(只定义一个的话 通常会产生意料之外的行为):

var myObject = { 
    // 给 a 定义一个getter 
    get a() { 
        return this._a_;  
    }, 
 
    // 给 a 定义一个setter 
    set a(val) { 
        this._a_ = val * 2; 
    }  
}; 
 
myObject.a = 2;  
 
myObject.a; // 4

4.存在性

前面我们介绍过,如myObject.a的属性访问返回值可能是undefined,但是这个值有可能 是属性中存储的undefined,也可能是因为属性不存在所以返回undefined。那么如何区分 这两种情况呢? 我们可以在不访问属性值的情况下判断对象中是否存在这个属性:

var myObject = {  
a:2 
}; 
("a" in myObject); // true 
("b" in myObject); // false  
myObject.hasOwnProperty( "a" ); // true 
myObject.hasOwnProperty( "b" ); // false

in 操作符会检查属性是否在对象及其[[Prototype]]原型链中。相比之下, hasOwnProperty(..) 只会检查属性是否在myObject 对象中,不会检查[[Prototype]] 链。

所有的普通对象都可以通过对于Object.prototype的委托来访问 hasOwnProperty(..),但是有的对象可能没有连接到Object.prototype(通过 Object. create(null) 来创建)。在这种情况下,形如myObejct.hasOwnProperty(..) 就会失败。

这时可以使用一种更加强硬的方法来进行判断:Object.prototype.hasOwnProperty. call(myObject,"a"),它借用基础的 hasOwnProperty(..) 方法并把它显式绑定到myObject上。

看起来in操作符可以检查容器内是否有某个值,但是它实际上检查的是某 个属性名是否存在。对于数组来说这个区别非常重要,4 in [2, 4, 6]的结 果并不是你期待的True,因为[2, 4, 6]这个数组中包含的属性名是0、1、 2,没有4。

1. 枚举 

先看代码:

var myObject = { }; 
 
Object.defineProperty( 
    myObject, 
    "a", 
    // 让a像普通属性一样可以枚举 
    { enumerable: true, value: 2 } 
); 
 
Object.defineProperty( 
    myObject, 
    "b", 
    // 让b不可枚举 
    { enumerable: false, value: 3 } 
); 
 
myObject.b; // 3 
("b" in myObject); // true  
myObject.hasOwnProperty( "b" ); // true 
 
// ....... 
 
for (var k in myObject) {  
    console.log( k, myObject[k] ); 
} 
// "a" 2

可以看到,myObject.b确实存在并且有访问值,但是却不会出现在for..in循环中(尽管 可以通过in操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。

也可以通过另一种方式来区分属性是否可枚举:

var myObject = { }; 
 
Object.defineProperty( 
    myObject, 
    "a", 
    // 让a像普通属性一样可以枚举 
    { enumerable: true, value: 2 } 
); 
 
Object.defineProperty( 
myObject, 
"b", 
// 让b不可枚举 
{ enumerable: false, value: 3 } 
); 
myObject.propertyIsEnumerable( "a" ); // true 
myObject.propertyIsEnumerable( "b" ); // false  
Object.keys( myObject ); // ["a"] 
Object.getOwnPropertyNames( myObject ); // ["a", "b"]

propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链 上)并且满足enumerable:true。

Object.keys(..) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举。

in 和hasOwnProperty(..) 的区别在于是否查找[[Prototype]] 链,然而,Object.keys(..) 和Object.getOwnPropertyNames(..) 都只会查找对象直接包含的属性。

(目前)并没有内置的方法可以获取in操作符使用的属性列表(对象本身的属性以 及[[Prototype]] 链中的所有属性)。不过你可以递归遍历某个对象的整条 [[Prototype]] 链并保存每一层中使用Object.keys(..)得到的属性列表——只包含可枚举属性。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值