首页>前端教程>JavaScript教程

对象属性的访问、遍历、getter和setter等高级特性!

现在都是框架和组件库的天下,很多学生一上来就说要学框架,对于原生代码嗤之以鼻,觉得都是上古时期的内容,枯燥难懂,效率低下,哪有框架用起来直接起飞。

现在前端这个岗位在2023年仿佛冰冻了一样,这段时间整理以前的资料,有时候也觉得干嘛不直接整理自动化构建和框架的内容,还把几年前的老内容放上来。

也许只是个情怀吧,毕竟自己在这上面走了很久的路,算是鞭尸了……

1、对象的属性和方法

如果访问的对象属性是一个函数,大家喜欢使用不一样的叫法以作区分。由于函数很容易被认为是属于某个对象,于是把这种函数称为“方法”。

从技术角度来说,函数永远不会“属于”一个对象,所以把对象内部引用的函数称为对象的方法有点不妥。

虽然函数具有this引用,有时候这些this确实会指向调函位置的对象引用。但是这种用法从本质上并没有把一个函数变成一个对象的“方法”,因为this是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。

无论对象访问属性返回什么值,每次访问对象的属性就是属性访问,如果属性访问返回的是一个函数,那它也并不是对象的一个“方法”。属性访问返回的函数和其它函数没有任何区别(除了可能发生的隐式绑定this)。

function foo(){
    console.log('foo');
}
var someFoo = foo;
var obj = {
    someFoo:foo
}
foo; // function foo(){...}
someFoo; //function foo(){...}
obj.someFoo; //function foo(){...}

someFoo和obj.someFoo只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属性”某个对象。

如果foo()定义时内部有一个this引用,那这两个函数引用的唯一区别就是obj.someFoo中的this会被隐式绑定到一个对象。

即使在对象中声明一个函数表达式,这个函数也不会“属性”这个对象,它们只是对于相同函数对象的多个引用。

var myobj = {
    foo:function(){
        console.log('foo');
    }
}
var someFoo = myobj.foo;
console.log(someFoo); // function foo(...)
console.log(myobj.foo);// function foo(...)

2、对象的复制

2.1 深复制

function anotherFunction(){
    /* ... */
}
var anotherObj = {
    c:true
}
var anotherArr = [];
var myObj = {
    a:2,
    b:anotherObj, // 引用,不是副本
    c:anotherArr, // 引用
    d:anotherFunction //引用
}
anotherArr.push(anotherObj,myObj);

对象之间互相引用,对于深复制来说,会由于循环引用导致死循环。

对于JSON安全的对象来说,有一种巧妙的复制方法:

var newObj = JSON.parse(JSON.stringfy(someObj));

这种方法需要保证对象是JSON安全的,所以只适用于部分情况。

2.2 浅复制

ES6定义了Object.assign()方法来实现浅复制。

该方法的第一个参数是目标对象,后面是一个或者多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键并把它们复制(使用=操作符赋值)到目标对象,最后返回目标对象。

就好像这样:

function anotherFunction(){
            //..
}
var anotherObj = {
    c:true
}
var anotherArr = [];
var myObj = {
    a:2,
    b:anotherObj, // 引用,不是副本
    c:anotherArr, // 引用
    d:anotherFunction //引用
}
anotherArr.push(anotherObj,myObj);


var newObj = Object.assign({},myObj);
console.log(newObj.a);   // 2
console.log(newObj.b === myObj.b);  //true
console.log(newObj.c === myObj.c);  //true
console.log(newObj.d === myObj.d);  //true

3、属性描述符

在ES5之前,JavaScript语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。

在ES5之后,所有的属性都具备了属性描述符。

var myObj = {
    a:2
}
var result = Object.getOwnPropertyDescriptor(myObj,'a');
console.log(result); //{value: 2, writable: true, enumerable: true, configurable: true}

这个普通的对象myObj的属性描述符不仅仅只有一个value值2,还包含另外三个特性:writable(可写)、enumerable(可枚举)、configurable(可配置)。

在我们创建普通属性时属性描述符会使用默认值。就等于如下代码:

var myObj = {};
Object.defineProperty(myObj,'a',{
    value:2,
    writable:true,
    configurable:true,
    enumerable:true
});
console.log(myObj.a); // 2

我们可以使用Object.defineProperty()来添加一个新属性,或者修改一个已有属性(如果这个属性是configurable)并对特性进行配置。

3.1 writable

writable决定是否可以修改属性的值。

var myObj = {};
Object.defineProperty(myObj,'a',{
    value:2,
    writable:false, //不可写
    configurable:true,
    enumerable:true
});
console.log(myObj.a); // 2
myObj.a = 3;
console.log(myObj.a); // 2

3.2 configurable

只要属性是可以配置的,就可以使用defineProperty()方法来修改属性描述符。

var myObj = {
    a:2
};
myObj.a = 3;
console.log(myObj.a);//3
Object.defineProperty(myObj,'a',{
    value:4,
    writable:true, 
    configurable:false, //不可配置
    enumerable:true
});
console.log(myObj.a); // 4
myObj.a = 5;
console.log(myObj.a); // 5

Object.defineProperty(myObj,'a',{
    value:6,
    writable:true,
    configurable:true,
    enumerable:true
})  //Uncaught TypeError: Cannot redefine property: a

尝试修改一个不可配置的属性描述符会出错。

把configurable修改成false是单向操作,无法撤销!!

var myObj = {
    a:2
};
myObj.a = 3;
console.log(myObj.a);//3
Object.defineProperty(myObj,'a',{
    value:4,
    writable:true, 
    configurable:false, //不可配置
    enumerable:true
});
console.log(myObj.a); // 4
myObj.a = 5;
console.log(myObj.a); // 5

Object.defineProperty(myObj,'a',{
    value:6,
    writable:false,  // 即使不可配置,还是可以把writable的状态由true改成false,但是无法由false改成true。
    configurable:false, //不可配置
    enumerable:true
})  
console.log(myObj.a); // 6

myObj.a = 7; //不可再修改
console.log(myObj.a); // 6

configurable:false还会禁止删除这个属性:

var myObj = {
    a:2
}
console.log(myObj.a); // 2
delete myObj.a;
console.log(myObj.a); // undefined
Object.defineProperty(myObj,'a',{
    value:3,
    writable:true,
    configurable:false,
    enumerable:true
});
console.log(myObj.a); // 3
delete myObj.a; //失效了,因为属性是不可配置的。
console.log(myObj.a); // 3

delete只用来删除对象的可删除的属性。如果对象的某个属性是某个对象/函数的最后一个引用者,当delete这个属性之后,这个未引用的对象/函数就可以被垃圾回收。

3.3 enumerable

这个属性描述符控制的是属性是否会出现在对象的属性枚举中。

比如说for...in循环,如果把enumerable设置为false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。

在谷歌调试工具中,不可枚举的属性是淡紫红色,可枚举的属性是深紫红色的。

var myObj = {};
Object.defineProperty(myObj,'a',{
    value:2,
    enumerable:true //让a像普通属性一样可以枚举
})
Object.defineProperty(myObj,'b',{
    value:3,
    enumerable:false, // 让b不可枚举
})
//可访问
console.log(myObj.b); // 3
//b这个属性在myObj对象中是否存在
console.log('b' in myObj); // true
//检测b这个属性是不是myObj自身的属性,而不是继承的属性
console.log(myObj.hasOwnProperty('b')); // true

for(var attr in myObj){
    console.log(attr,myObj[attr]); // a 2 ,b没有被遍历出来
    }

myObj.b可以被访问,确实存在,但是不会出现在for...in循环中,因为只有可枚举的属性才能出现在对象属性的遍历中。

还有一些方法可以区分属性是否可枚举:

var myObj = {};
Object.defineProperty(myObj,'a',{
    value:2,
    enumerable:true,
})
Object.defineProperty(myObj,'b',{
    value:3,
    enumerable:false,
})
console.log(myObj);
// propertyIsEnumerable()检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
console.log(myObj.propertyIsEnumerable('a')); // true
console.log(myObj.propertyIsEnumerable('b')); // false
//返回所有可枚举的属性的数组。只查找对象直接包含的属性,不查找原型链。
console.log(Object.keys(myObj)); //['a']
//返回一个数组,包含所有属性,无论是否可枚举。只查找对象直接包含的属性,不查找原型链。
console.log(Object.getOwnPropertyNames(myObj)); //['a', 'b']

4、遍历

Object.keys(obj)可以返回obj对象自身可遍历的属性的数组。

Object.getOwnPropertyNames(obj) 可以返回对象自身所有属性,包括不可遍历的属性,也是返回数组。

也可以用for循环去遍历对象属性。

for...in可以遍历对象的可枚举属性(包括prototype原型链),无法直接获取属性值,需要手动获取属性值。

4.1 for...in

对于数组来说,使用标准的for循环来遍历值:

let arr = [1,2,3];
for(let i = 0;i < arr.length; i++){
    console.log(arr[i]);
}

ES5中增加了一些数组的辅助迭代器,包括forEach()、every()、some()、map()、filter()、reduce()等。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。

forEach()会遍历数组中的所有值并忽略回调函数的返回值。

every()会一直运行直到回调函数返回false。

some()会一直运行直到回调函数返回true。

不要用for...in遍历数组,因为它不仅遍历数字索引,还会遍历出数组的其它属性。

let arr = [1,2,3];
arr.name = 'myArr';
arr.id = '001';
for(let attr in arr){
    console.log(attr);//'0' '1' '2' 'name' 'id'
}

用for循环或者forEach()这种迭代器,遍历数组下标时采用的是数字顺序,但是遍历对象属性时的顺序是不确定的。在不同的JavaScript引擎中可能不一样。

因此,在任何不同的环境中需要保证一致性时,一定不要相信任何观察到的属性的顺序,它们是不可靠的。

4.2 for...of

ES6增加了一种用来遍历数组的for...of循环语句,可以直接遍历值而不是数组的下标(或者对象的属性,如果对象本身定义了迭代器的话也可以遍历对象)。

let arr = [1,2,3];
for(let val of arr){
    console.log(val);
}

for...of循环首先会向被访问的对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。

数组有内置的@@iterator,因此for...of可以直接应用在数组上。

使用内置的@@iterator来手动遍历数组,看看它是如何工作的:

let arr = [1,2,3];
//数组的实例有这个属性:Symbol(Symbol.iterator):f values()
let it = arr[Symbol.iterator]();
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: 3, done: false}
console.log(it.next()); // {value: undefined, done: true}

使用ES6中的符号Symbol.iterator来获取对象的@@iterator内部属性,引用类似iterator这种特殊的属性时要使用Symbol符号名。

@@iterator本身并不是一个迭代器对象,而是一个返回迭代器对象的函数。函数执行之后,返回的才是迭代器对象。

然后调用迭代器对象的next()方法会返回{value:..,done:..}的值,value是当前遍历的值,done是一个布尔值,表示是否还有可以遍历的值。

和数组不同,普通的对象没有内置的@@iterator,所以无法自动完成for...of遍历,主要是为了避免影响未来的对象类型。

可以为任何想遍历的对象定义@@iterator。

let myObj = {
    a: 2,
    b: 3
}
Object.defineProperty(myObj, Symbol.iterator, {
    //这种属性不需要被枚举
    enumerable: false,
    writable: false,
    configurable: true,
    //设置该迭代器的值是一个函数
    value: function () {
        var o = this;
        var idx = 0;
        //返回所有可枚举的自身属性的数组
        var ks = Object.keys(o);
        //迭代器函数执行之后返回一个对象
        return {
            //对象里面包含一个next函数,函数返回一个对象
            //这里构成了闭包
            next: function () {
                return {
                    value: o[ks[idx++]],
                    done: (idx > ks.length)
                };
            }
        };
    }
});

//手动遍历对象
let it = myObj[Symbol.iterator]();
console.log(it.next());//{value: 2, done: false}
console.log(it.next());//{value: 3, done: false}
console.log(it.next());//{value: undefined, done: true}

//用for...of遍历对象
for(let val of myObj){
    console.log(val);
}

定义一个“无限”迭代器,它永远不会“结束”并且总会返回一个新值(随机数、递增数、唯一标识符等等)。

// 无限迭代器
let randoms = {
    [Symbol.iterator]:function(){
        return {
            next:function(){
                return {value:Math.random()};
            }
        };
    }
}

let pool = [];

for(let val of randoms){
    pool.push(val);
    //防止无限运行
    if(pool.length === 100) break;
}
console.log(pool)

//简写形式
let nums = [];
for(;;){
    nums.push(Math.random());
    if(nums.length == 100){
        break;
    }
}
console.log(nums);

5、[[Get]]和[[Put]]

5.1 [[Get]]

属性访问在实现时有一个微妙却非常重要的细节:

var myObj = {
    a:2
}
console.log(myObj.a);

这是一次属性的访问,但是并不仅仅是在myObj中查找名字为a的属性。

myObj.a在myObj上实际上是实现了[[Get]]操作。对象默认的内置[[Get]]操作首先在对象中查找是否有相同名称的属性,如果找到就会返回这个属性的值。

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

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

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

var myObj = {
    a:undefined
}


console.log(myObj.a); //undefined
console.log(myObj.b); //undefined

console.log('a' in myObj); // true
console.log('b' in myObj); //false

console.log(myObj.hasOwnProperty('a')); // true
console.log(myObj.hasOwnProperty('b')); //false

in操作符会检查属性是否在对象及其[[Prototype]]原型链上。hasOwnProperty()只会检查属性是否在myObj对象上,不会检查[[Prototype]]原型链。

5.2 [[Put]]

既然有获取属性值的[[Get]]操作,就有对应的[[Put]]操作。

给对象的属性赋值会触发[[Put]]操作,实际的行为取决于许多因素,包括对象中是否已经存在这个属性。

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

1、属性是否是访问描述符?如果是并且存在setter就调用setter。

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

3、如果都不是,将该值设置为属性的值。

如果对象中不存在这个属性,[[Put]]操作会更加复杂。

6、Getter和Setter

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

在ES5中可以适用getter和setter改写默认操作。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。

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

var myObj = {
    //给属性a定义一个getter
    get a(){
        return 2;
    }
}
Object.defineProperty(myObj,'b',{
    //给b定义一个getter
    get:function(){
        return this.a * 2;
    },
    //属性b可以枚举
    enumerable:true
})

console.log(myObj.a); // 2
console.log(myObj.b); // 4

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

var myObj = {
     //默认的[[Get]]操作会被getter覆盖
     a:22,
     //给属性a定义一个getter
     get a(){
         return 2;
     },

 }
 Object.defineProperty(myObj,'b',{
     //给b定义一个getter
     get:function(){
         return this.a * 2;
     },

     //属性b可以枚举
     enumerable:true
 })

console.log(myObj.a); // 2
//只定义了getter,对属性a进行设置时set操作会忽略赋值操作
myObj.a = 3;
console.log(myObj.a); // 2
console.log(myObj.b); // 4

为了让属性更合理,还应当定义setter,setter会覆盖单个属性默认的[[Put]]赋值操作。

通常来说getter和setter是成对出现的。

var myObj = {
    get a() {
        //this._a,不能是this.a,否则会接着调用get,然后陷入死循环报错,超过堆栈大小范围。
        return this._a;
    },
    set a(val) {
        this._a = val * 2;
    }
};
myObj.a = 2;
console.log(myObj.a);

7、原型链

7.1[[Prototype]]

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。

几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值,除了Object.create(null)。

我们在访问对象的属性时:

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

当试图访问对象的属性时会触发[[Get]]操作,对于默认的[[Get]]操作来说,第一步就是检查对象本身是否有这个属性, 如果有的话就使用它。

如果a不在myObject中,就需要使用[[Prototype]]原型链了。

let anotherObject = {
    a:2
};
//创建一个关联到anotherObject的对象
let myObject = Object.create(anotherObject);
console.log(myObject.a); // 2

Object.create()会创建一个对象并把这个对象的[[Prototype]]关联到指定的对象。是ES5新增的函数。

相当于如下代码:

//polyfill
// 如果这个函数不存在
if(!Object.create){
    // 创建一个函数
    Object.create = function(o){
        // 创建一个构造函数
        function F(){}
        // 把构造函数的prototype原型对象指向传递进来的o对象
        F.prototype = o;
        // 返回构造函数的一个实例
        return new F();
    }
}

如果anotherObject也找不到a,并且[[Prototype]]原型链不为空null的话,就会继续查找下去。如果找完了整条原型链都没有,则会返回undefined.

for...in遍历对象,任何可以通过 原型链访问到的可枚举的属性都会被遍历。使用in操作符来检查属性是否在对象中存在时,同样会检查对象的整条原型链。

let supperObject = {
    a: 2
};
//supperObject是anotherObject的原型
let anotherObject = Object.create(supperObject, {
    b: {
        value: 3,
        enumerable: false,
        writable: false,
        configurable: true
    }
});
//anotherObject是myObject的原型对象
let myObject = Object.create(anotherObject);
myObject.c = 4;

for (let attr in myObject) {
    console.log(attr); // c a 
}

console.log('b' in myObject); // true

7.2 Object.prototype

所有对象的[[Prototype]]链最终都会指向内置的Object.prototype。由于所有的对象都源于这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。

比如valueOf()、toString()、hasOwnProperty()等都是任何对象实例可以访问的方法。

原型对象中的方法[[Prototype]]: Object 是所有实例可以访问的,constructor属性指向的构造函数下面的方法,比如constructor: ƒ Object() 构造函数下面的方法只有Object对象本身才能访问,实例不能访问。

7.3 属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。

myObject.b = 4;

如果myObject对象中包含名为b的普通数据访问属性,这条赋值语句只会修改已有的属性值。

如果b不是直接存在于myObject中,[[Prototype]]链就会被遍历,类似[[Get]]操作,如果原型链上找不到b,则把b直接添加到myObject上。

如果b存在于原型链上层,赋值语句的行为就会有些 不同。

如果属性名b即出现在myObject中,也出现在myObject的[[Prototype]]链上层,就会发生屏蔽。

myObject中出现的b属性会屏蔽原型链上层的所有b属性,因为myObject.b总是会选择原型链中最底层的b属性。

如果在[[Prototype]]链上层存在名为b的普通数据访问属性,并且没有被标记为只读(writable:false),那就会直接在myObject上添加一个名为b的新属性,它是屏蔽属性。

let supperObject = {
    a: 2
};
let anotherObject = Object.create(supperObject, {
    b: {
        value: 3,
        enumerable: true,
        writable: true,
        configurable: true
    }
});

let myObject = Object.create(anotherObject);
myObject.b = 4;

console.log(myObject.b); // 4

如果在[[Prototype]]链上层存在名为b的普通数据访问属性,并且被标记为只读(writable:false),那么无法修改已有属性或者在myObject上创建屏蔽属性。这条赋值语句会被忽略,不会发生屏蔽。

let supperObject = {
    a: 2
};
let anotherObject = Object.create(supperObject, {
    b: {
        value: 3,
        enumerable: true,
        writable: false,
        configurable: true
    }
});

let myObject = Object.create(anotherObject);
myObject.b = 4;

console.log(myObject.b); // 3

如果在[[Prototype]]链上层存在b,并且它是一个setter,那就一定会调用这个setter,b不会被添加到myObject上。

let supperObject = {
    a: 2
};
let anotherObject = Object.create(supperObject, {
    b: {
        value: 3,
        enumerable: false,
        writable: false,
        configurable: true,
    }
});
Object.defineProperty(anotherObject, 'b', {
    get: function () {
       //_b避免直接使用b导致死循环 
        return this._b;
    },
    set: function (val) {
        this._b = val * 3;
    }
})

let myObject = Object.create(anotherObject);
myObject.b = 4; 
console.log(myObject.b); // 12

上面的限制只存在于myObject.b = 4这种赋值语句中,使用Object.defineProperty()则不受限制。

let supperObject = {
    a: 2
};
let anotherObject = Object.create(supperObject, {
    b: {
        value: 3,
        enumerable: false,
        writable: false,
        configurable: true,
    }
});
Object.defineProperty(anotherObject, 'b', {
    get: function () {
        return this._b;
    },
    set: function (val) {
        this._b = val * 3;
    }
})

let myObject = Object.create(anotherObject);
myObject.b = 4;

console.log(myObject.b); // 12
//这种方式则会产生屏蔽,不受writable:false和setter的影响。
Object.defineProperty(myObject, 'b', {
    value: 5,
    writable: true,
    enumerable: true,
    configurable: true
})
console.log(myObject.b) // 5


点赞


1
保存到:

相关文章

发表评论:

◎请发表你卖萌撒娇或一针见血的评论,严禁小广告。

Top