MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript属性访问表达式的深入解析

2021-12-294.5k 阅读

JavaScript 属性访问表达式的基本概念

在 JavaScript 中,属性访问表达式用于获取对象的属性值或对对象的属性进行赋值操作。这是 JavaScript 面向对象编程和数据操作的核心机制之一。

点表示法(Dot Notation)

点表示法是最常见的属性访问方式,语法形式为 object.property。其中,object 是对象的引用,property 是要访问的属性名。

例如,假设有一个简单的对象:

let person = {
    name: 'John',
    age: 30
};
console.log(person.name); 

在上述代码中,person 是一个对象,name 是该对象的属性。通过 person.name,我们可以获取 person 对象中 name 属性的值。

点表示法具有以下特点:

  1. 属性名的限制:使用点表示法时,属性名必须是一个有效的 JavaScript 标识符。这意味着属性名不能以数字开头,不能包含空格或特殊字符(除了 $_)。例如:
let obj = {
    validProp: 'value',
    // 以下属性名使用点表示法会报错
    // 1invalid: 'value', 
    // space prop: 'value' 
};
console.log(obj.validProp); 
  1. 语法简洁:点表示法简洁明了,易于阅读和编写,适用于大多数常规属性名的访问场景。

方括号表示法(Bracket Notation)

方括号表示法的语法形式为 object['property']。同样,object 是对象的引用,而 'property' 是用字符串表示的属性名。

例如:

let person = {
    name: 'John',
    age: 30
};
console.log(person['name']); 

方括号表示法的优势在于:

  1. 灵活的属性名:可以使用任何字符串作为属性名,包括不符合标识符规则的字符串。例如:
let obj = {
    '1valid': 'value',
  'space prop': 'value'
};
console.log(obj['1valid']); 
console.log(obj['space prop']); 
  1. 动态属性访问:方括号内可以是任何表达式,其结果会被转换为字符串作为属性名。这使得我们可以在运行时动态地确定要访问的属性。例如:
let person = {
    name: 'John',
    age: 30
};
let propName = 'name';
console.log(person[propName]); 

属性访问表达式与原型链

JavaScript 的对象是基于原型链的。当使用属性访问表达式获取一个对象的属性时,如果在该对象本身没有找到该属性,JavaScript 会沿着原型链向上查找,直到找到该属性或到达原型链的顶端(null)。

原型链上的属性查找

假设有如下代码:

function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(this.name +'makes a sound.');
};
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
let myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.name); 
myDog.speak(); 

在上述代码中,myDogDog 的实例。当我们访问 myDog.name 时,由于 myDog 对象本身有 name 属性(在 Dog 构造函数中定义),所以直接返回该属性值。而当我们调用 myDog.speak() 时,myDog 对象本身没有 speak 方法,JavaScript 会沿着原型链查找。首先在 Dog.prototype 中查找,没有找到;然后在 Animal.prototype 中找到 speak 方法并执行。

影响原型链查找的因素

  1. 属性遮蔽(Property Shadowing):如果对象本身定义了与原型链上同名的属性,那么对象本身的属性会遮蔽原型链上的属性。例如:
function Animal() {
    this.color = 'grey';
}
function Dog() {
    this.color = 'brown';
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
let myDog = new Dog();
console.log(myDog.color); 

在上述代码中,myDog 对象本身的 color 属性遮蔽了 Animal.prototype 中的 color 属性,所以输出 brown

  1. hasOwnProperty 方法:可以使用 hasOwnProperty 方法来判断对象本身是否拥有某个属性,而不考虑原型链。例如:
function Animal() {
    this.color = 'grey';
}
function Dog() {
    this.color = 'brown';
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
let myDog = new Dog();
console.log(myDog.hasOwnProperty('color')); 
console.log(myDog.hasOwnProperty('speak')); 

上述代码中,myDog 本身有 color 属性,所以 myDog.hasOwnProperty('color') 返回 true;而 speak 方法在原型链上,所以 myDog.hasOwnProperty('speak') 返回 false

属性访问表达式中的计算属性

在 ES6 中引入了计算属性的概念,这在对象字面量定义和属性访问中提供了更多的灵活性。

对象字面量中的计算属性

在对象字面量中,可以使用方括号来定义计算属性。例如:

let key = 'name';
let person = {
    [key]: 'John',
    age: 30
};
console.log(person.name); 

在上述代码中,通过 [key] 这种形式,将变量 key 的值作为属性名来定义 person 对象的属性。这在需要动态生成属性名的场景下非常有用。

与属性访问表达式的结合

计算属性与属性访问表达式紧密相关。在属性访问时,如果使用方括号表示法,方括号内的表达式本质上就是在计算属性名。例如:

let person = {
    name: 'John',
    age: 30
};
let propExpr = 'name';
console.log(person[propExpr]); 

这里 propExpr 是一个表达式,其值作为属性名用于访问 person 对象的属性。

属性访问表达式与作用域

属性访问表达式的行为还会受到作用域的影响。特别是在函数内部,this 的指向会影响属性访问的结果。

this 在属性访问中的作用

在函数内部,this 的值取决于函数的调用方式。例如:

let obj = {
    value: 42,
    printValue: function() {
        console.log(this.value);
    }
};
obj.printValue(); 

在上述代码中,printValue 函数内部的 this 指向 obj,所以 this.value 访问的是 objvalue 属性。

然而,如果函数作为普通函数调用,this 的指向会有所不同。在非严格模式下,this 会指向全局对象(在浏览器环境中是 window);在严格模式下,this 会是 undefined。例如:

function printValue() {
    console.log(this.value);
}
let value = 10;
printValue(); 

在上述代码中,由于 printValue 作为普通函数调用,在非严格模式下,this 指向全局对象 windowwindow.value10,所以输出 10

箭头函数与 this

箭头函数与普通函数在 this 的处理上有很大的不同。箭头函数没有自己的 this,它的 this 继承自外层作用域。例如:

let obj = {
    value: 42,
    getValue: function() {
        return () => this.value;
    }
};
let innerFunc = obj.getValue();
console.log(innerFunc()); 

在上述代码中,箭头函数 () => this.value 中的 this 继承自 getValue 函数的作用域,而 getValue 函数是作为 obj 的方法调用的,所以 this 指向 obj,最终输出 42

属性访问表达式的性能考虑

在实际编程中,性能是一个重要的考虑因素。不同的属性访问表达式在性能上可能会有差异。

点表示法与方括号表示法的性能比较

一般来说,点表示法的性能略高于方括号表示法。因为点表示法在解析时,属性名是固定的,JavaScript 引擎可以进行一些优化。而方括号表示法中的属性名是动态计算的,需要额外的处理。

例如,在一个循环中进行属性访问:

let obj = {
    prop1: 'value1',
    prop2: 'value2',
    prop3: 'value3'
};
// 使用点表示法
let start = Date.now();
for (let i = 0; i < 1000000; i++) {
    obj.prop1;
}
let end = Date.now();
console.log('Dot notation time:'+ (end - start) +'ms');
// 使用方括号表示法
start = Date.now();
for (let i = 0; i < 1000000; i++) {
    obj['prop1'];
}
end = Date.now();
console.log('Bracket notation time:'+ (end - start) +'ms');

上述代码通过循环多次访问属性,并记录使用点表示法和方括号表示法的时间。在大多数情况下,点表示法的执行时间会更短。

原型链属性查找的性能影响

每次通过属性访问表达式查找原型链上的属性时,都会涉及到一定的性能开销。因为 JavaScript 引擎需要沿着原型链逐个查找,直到找到属性或到达原型链顶端。如果原型链很长,或者在循环中频繁进行原型链属性查找,性能会受到较大影响。

例如:

function Base() {}
Base.prototype.prop = 'value';
function Child1() {}
Child1.prototype = Object.create(Base.prototype);
function Child2() {}
Child2.prototype = Object.create(Child1.prototype);
let child2Instance = new Child2();
// 循环查找原型链属性
let start = Date.now();
for (let i = 0; i < 1000000; i++) {
    child2Instance.prop;
}
let end = Date.now();
console.log('Prototype chain lookup time:'+ (end - start) +'ms');

在上述代码中,child2Instance 查找 prop 属性需要沿着较长的原型链查找。多次循环查找会增加性能开销。

为了优化性能,可以尽量避免在性能敏感的代码中进行深层原型链的属性查找,或者缓存经常访问的原型链属性。

属性访问表达式与异常处理

在使用属性访问表达式时,可能会遇到一些异常情况,需要适当的异常处理机制。

访问不存在的属性

当访问一个对象不存在的属性时,JavaScript 不会抛出错误,而是返回 undefined。例如:

let obj = {
    name: 'John'
};
console.log(obj.age); 

在上述代码中,obj 对象没有 age 属性,所以 obj.age 返回 undefined

然而,如果在使用属性访问表达式时,对象本身为 nullundefined,则会抛出 TypeError。例如:

let nullObj = null;
// 以下代码会抛出 TypeError
// console.log(nullObj.name); 

异常处理的方式

为了避免因属性访问导致的 TypeError,可以在访问属性之前进行对象是否存在的检查。例如:

let maybeNullObj = null;
if (maybeNullObj) {
    console.log(maybeNullObj.name); 
}

或者使用可选链操作符(Optional Chaining Operator),这是 ES2020 引入的新特性。例如:

let maybeNullObj = null;
console.log(maybeNullObj?.name); 

可选链操作符 ?. 会在对象为 nullundefined 时,直接返回 undefined,而不会抛出错误,从而简化了属性访问的异常处理。

属性访问表达式的特殊情况

访问数组的属性

数组在 JavaScript 中也是对象,除了通过索引访问元素外,也可以有自定义的属性。例如:

let arr = [1, 2, 3];
arr.customProp = 'value';
console.log(arr.customProp); 

在上述代码中,我们为数组 arr 定义了一个自定义属性 customProp,并通过属性访问表达式获取其值。

需要注意的是,数组的索引本质上也是属性,并且遵循一定的规则。数组的索引是从 0 开始的无符号 32 位整数,并且 length 属性会根据数组元素的添加和删除自动更新。例如:

let arr = [1, 2, 3];
console.log(arr.length); 
arr.push(4);
console.log(arr.length); 

访问函数的属性

函数在 JavaScript 中也是对象,可以有自己的属性。例如:

function myFunction() {
    console.log('Function executed');
}
myFunction.customProp = 'value';
console.log(myFunction.customProp); 

函数的属性可以用于各种目的,比如存储一些与函数相关的元数据或状态。例如,一个函数可能需要记录它被调用的次数:

function counter() {
    counter.callCount = (counter.callCount || 0) + 1;
    console.log('Call count:'+ counter.callCount);
}
counter(); 
counter(); 

在上述代码中,counter 函数通过访问自身的 callCount 属性来记录调用次数。

总结

JavaScript 的属性访问表达式是一个基础而又重要的概念,掌握点表示法、方括号表示法、原型链查找、计算属性、作用域影响、性能考虑、异常处理以及特殊情况等方面,对于编写高效、健壮的 JavaScript 代码至关重要。无论是简单的对象属性操作,还是复杂的面向对象编程和大型项目开发,对属性访问表达式的深入理解都能帮助开发者更好地实现功能并优化代码。在实际应用中,根据不同的场景选择合适的属性访问方式,并注意可能出现的性能和异常问题,能够提升代码的质量和运行效率。