JavaScript基础之 对象(一)

239 阅读10分钟

对象

概念

对象是具有一些特殊特性的而关联数组。
她们存储属性(键值对),其中:

  1. 属性的键必须是字符串或者symbol
  2. 值可以是任何类型。

创建对象

我们可以用下面两种语法中的任一种来创建一个空的对象:

let user = new Object(); //构造函数的语法
let user = {}; //字面量的语法

访问属性

我们可以使用两种方法访问属性:

  1. 点符号 obj.property
  2. 方括号 obj["property"] 🔺点符号要求key是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用$_

例如:

user.likes birds = true // 提示语法错误
user["likes birds"] = true

🔺方括号同样提供了一种可以通过任意表达式来获取属性名的方法。
比如,变量key可以是程序运行时计算得到的,也可以是根据用户的输入得到的:

let user = {
  name: "John",
  age: 30,
};

let key = prompt("What do you want to know about the user?", "name");

alert( user[key] ); // John
alert( user.key ); // undefined [点符号不饿能以类似的方式使用]

🔺方括号中使用更复杂的表达式

let fruit = "apple";
let bag = {
    [fruit + "Computers"]: 5 //appleComputers: 5
};

其他操作

  1. 删除属性:delete obj.prop
let user = {
    name: "John",
    age: 30
}

delete user.age; //删除age属性
  1. 检查是否存在给定键的属性:key in obj
//我们可以对上一步删除操作进行验证

alert( "age" in user ); // false
alert( "name" in user ); // true
  1. 遍历对象:for(let key in obj)循环
let user = {
    name: "John",
    age: 18,
    isAdmin: true
};

for(let key in user){
    alert(key); //name age isAdmin
    alert(user.key); //John 18 true
}

❓如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?
简短的回答是:“有特别的顺序”:整数属性会被进行排序,其他属性则按照创建的顺序显示

整数属性是什么?
这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。

// Math.trunc 是内置的去除小数部分的方法。   
alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性 
alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性 
alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性

例如:让我们考虑一个带有电话号码的对象

let codes = {
    "49": "Germany",
    "41": "Switzerland",
    "44": "Great British",
    "1": "USA"
};

for(let code in codes){
    alert(code);  // 1, 41, 44, 49
}

原文链接:zh.javascript.info/object

对象的引用和复制

与原始类型相比,对象的根本区别之一是对象是“通过引用”被存储和复制的,与原始类型值相反:字符串,数字,布尔值等——始终是以“整体值”的形式被复制的。
赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址”,换句话说就是对该对象的引用。
当一个对象变量被复制——引用则被复制,而该对象并没有被复制。

//message和phrase是两个独立的变量    
    let message = "Hello";
    let phrase = message;

    alert(message); //Hello
    alert(phrase); //Hello

    phrase = "Hello World";

    alert(message); //Hello
    alert(phrase); //Hello World
//user和admin是对同一个的对象的引用,我们可以通过任意一个变量来访问该对象并修改它的内容
    let user = { name: 'John' };
    let admin = user;
    
    admin.name = 'Pete'; // 通过 "admin" 引用来修改
    
    alert(user.name); // 'Pete',修改能通过 "user" 引用看到

通过引用来比较

仅当两个对象同一对象时,两者才相等。
例如,这里ab两个变量都引用同一个对象,所以他们相等:

let a = {};
let b = a;

alert(a == b); // true
alert(a === b); // true

而这里两个独立的对象则并不相等,即使他们看起来很像(都为空):

let a = {};
let b = {};

alert(a == b); // false

克隆与合并,Object.assign

❓拷贝一个对象变量会又创建一个对相同对象的引用。如果我们想要复制一个对象,那该怎么做?
1. 创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。 就像这样:

let user = {
    name: "John",
    age: 30
};

let clone = {}; // 创建新的空对象

// 将user中所有的属性拷贝的其中
for(let key in user){
    clone[key] = user[key];
}

clone.name = "Pete";

alert(clone.name);  //Pete
alert(user.name);  //John

2. Object.assign方法

let user = {
    name: "John",
    age: 30
};

let clone = Object.assign({}, user);

clone.name = "Pete";

alert(clone.name);  //Pete
alert(user.name);  //John

🔺Object.assign 语法
Object.assign(dest, [src1, src2...])

  • 第一个参数dest是指目标对象
  • 更后面的参数src1,src2...(可按需传递多个参数)是源对象
  • 该方法将所有源对象的属性拷贝到目标对象dest中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。
  • 调用结果返回dest 例如,我们可以用它来合并多个对象:
let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

Object.assign(user, permissions1, permissions2);

for(let key in user){
    console.log(key + ":" + user[key]);
}
/* 控制台输出
name:John
canView:true
canEdit:true
*/

如果被拷贝的属性的属性名已存在,那么它会被覆盖:

let user = { name: "John" }; 
Object.assign(user, { name: "Pete" }); 
alert(user.name);   // Pete

深拷贝

到现在为止,我们都假设user的所有属性均为原始类型,但属性可以是对其他对象的引用,那应该怎样处理它们呢?

let user = {
   name: "John",
   age: 18,
   sizes: {
      height: 182,
      weight: 70
   }
};

let clone = Object.assign({}, user);

alert(user.sizes === clone.sizes); // true

user.sizes.weight++;
alert(clone.sizes.weight); // 71

现在这样拷贝clone.sizes = user.sizes已经不足够了,因为user.sizes是个对象,它会以引用的形式被拷贝,因为clone和user会共用一个sizes。

为了解决此问题,我们应该使用会检查每个user[key]的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。

我们可以用递归来实现。或者不自己造轮子,使用现成的实现,例如Javascript库lodash中的_.cloneDeep(obj)

原文链接:zh.javascript.info/object-copy

对象方法

通常创建对象来表示真是世界中的实体。在现实生活中,用户可以运行操作:从购物车中挑选某物,登录,注销等。 在JavaScript中,行为(action)由属性中的函数来表示。

let user = {
    name: "John",
    age: 18
}

user.sayhi = function() {
    alert( "Hello!" );
}

user.sayhi(); // Hello!

在这里我们使用函数表达式创建了一个函数,并将其指定给对象的user.sayhi属性。随后我们像这样user.sayhi()调用它。
作为对象属性的函数被称为方法
我们也可以使用预先声明的函数作为方法:

let user = {
    name: "John",
    age: 18
}

// 首先,声明函数
function sayhi(){
    alert( "Hello!" );
}

// 然后将其作为一个方法添加
user.sayhi = sayhi;

user.sayhi(); // Hello!

在对象字面量中,有一种更短的(声明)方法的语法:

let user = {
    name: "John",
    age: 18,
    sayhi(){ alert( "Hello!" ) }
};

user.sayhi(); // Hello!

说实话,这种表示法还是有些不同。在对象继承方面又一些细微的差别。

this

通常,对象方法需要访问对象中存储额信息才能完成其工作。 为了访问该对象,方法中可以使用this关键字。
this的值就是在点之前的这个对象,即调用该方法的对象。

let user = {
    name: "John",
    age: 18,
    sayhi(){ alert( "Hello! " + this.name ) }
};

user.sayhi(); // Hello! John

this不受限制

在JavaScript中,this关键字与其他大多数变成语言中的不同。JavaScript中的this可以用于任何函数,即使他不是对象的方法。

下面这样的代码没有语法错误:

function sayhi(){
    alert( this.name )
}

this的值是在代码运行时计算出来的,他取决于代码上下文。
例如,这里相同的函数被分配给两个不同的对象,在调用中有着不同的this值。

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi(){
    alert( this.name );
}

// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;

// 这两个调用有不同的 this 值
// 函数内部的 this 是 点符号前面的那个对象
user.f(); // John
admin.f(); // Admin

这个规则很简单:如果obj.f()被调用了,则thisf函数嗲用期间是obj。所以在上面的例子中this先是user,之后是admin

在没有对象的情况下调用:this == undefined
我们甚至可以在没有对象的情况下调用函数:

function sayhi(){
   alert( this )
}

sayhi(); // undefined

在这种情况下,严格模式下的this值为undefined。如果我们尝试访问this.name,将会报错。

"use strict"
function sayhi() {
   alert(this.name)
}

sayhi(); // Uncaught TypeError: Cannot read property 'name' of undefined

在非严格模式下的情况,this将会是全局对象(浏览器中的window)。这是一个历史行为,"use strict"已经将其修复了。

function sayhi() {
   alert(this.name)
}

sayhi(); // 没有语法错误

通常这种调用时程序出错了。如果在一个函数内部有this,那么通常意味着它是在对象上下文环境中被调用的。

箭头函数没有自己的this

箭头函数有些特别:它们没有自己的this。如果我们在这样的函数中引用this,this值取决于外部“正常的”函数。
举个例子,这里的arrow()使用的this来自于外部的user.sayHi()方法:

let user = {
    firstName: "Ilya",
    sayHi(){
        let arrow = () => { alert(this.firstName) };
        arrow();
    }
};

user.sayHi(); // Ilya

这是箭头函数的一个特性,当我们并不想要一个独立的this,反而想从外部上下文中获取时,它很有用。

原文链接:zh.javascript.info/object-meth…

垃圾回收

可达性

JavaScript中主要的内存管理概念是可达性
简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

  1. 这里列出固有的可达值的基本集合,这些值明显不能被释放。 比方说:
  • 当前函数的局部变量和参数
  • 嵌套调用时,所有调用链上所有函数的变量与参数
  • 全局变量
  • (还有一些内部的) 这些值被称作根(roots)
    如果一个值可以通过引用或引用链从根访问任何其它值,则认为该值是可达的。
  1. 如果一个值可以通过引用或引用链从根访问任何其它值,则认为该值是可达的。
    比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则该对象被认为是可达的。而且它引用的内容也是可达的。
    在JavaScript引擎中有一个被称作垃圾回收器的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。

内部算法

垃圾回收的基本算法被称为mark-and-sweep
定期执行以下“垃圾回收”步骤:

  • 垃圾收集器找到所有的根,并标记它们
  • 然后它遍历并标记来自它们的所有引用
  • 然后它遍历标记的对象并标记它们的引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
  • ......如此操作,直到所有可达的(从根部)引用都被访问到
  • 没有被标记的对象将会被删除

主要需要掌握的内容:

  • 垃圾回收是自动完成的,我们不能强制执行或者组织执行
  • 当对象是可达状态时,它一定是存在于内存中的
  • 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体不可达。

详细内容参考原文:zh.javascript.info/garbage-col…