Javascript之对象基础

对象

创建对象的两种方式:

1
2
3
4
5
let user = new Object(); // 构造函数法
let user = {
name: "Tom",
age: 20,
}; // 健/值

属性的访问和设置值,删除变量,有两种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// method 1:
user.name = "Jim"; // set
console.log(user.name); // get
delete user.name;

// method 2:
user["name"] = "jim";
console.log(user["name"]);
delete user["name"];

// method 2好处:
const key = "name";
user[key] = "Jimmy";
console.log(user[key])
// *** 这种方式,可以将key用变量代替。方括号中的内容可以由计算得出。如:
console.log(user["na" + "me"]);

属性值的限制和简写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 在能过{}方式创建对象时,可使用多字词来做为属性的key
const user = {
name: "Tom",
"come from": "China",
age: 22
}
// 但访问和设置值时,不能使用 "." 来访问,如下是错误的:
user.com from = "New name"; // <--- 报错

但可以使用方括号的方式:
user["come from"] = "US";

const key = "come from";
console.log(user[key]);

// 普通变量名不能使用 let, in 等关键字,但属性名可以使用,无此限制:
const user = {
for: 123
};

// 如果创建的key/value名字相同,可简写。如:
const name = "Tom";
const age = 22;
const user = {
name:name,
age:age
};
// 可简写为:
const user = {
name,
age,
};
// 在函数中更方便使用:
function makeUser(name, age) {
return {
name,
age
}
}

循环获取对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let user = {
name: "Tom",
5: 55555,
age: 10
}
// 获取值,可使用 for…in来循环,如下:
for(let k in user) {
console.log(k);
}
/* 输出时,整数属性的key会放在前面(已经转为了字符串型),其余按创建顺序:
5
name
age
*/

// 判断一个key是否存在:
console.log("name" in user); // output: true

引用和复制

对象的值是引用。

1
2
3
4
5
6
7
8
9
10
11
12
let user1 = {
name: "Tom",
age: 10,
};

let user2 = user1;

user2.age = 20;
console.log(user1.age); // output: 20

// 对象的比较,是比较两个对象是否是引用了同一个对象。
console.log(user1 == user2); //output: true

如果要通过赋值,将两个对象分来,就需要使用克隆。javascript内置有Object.assign,这这个克隆不够深:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let user1 = {
name: "Tom",
age: 10,
child:{
name: "apple"
}
};

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

user1.name = "Jimmy";
user1.child.name = "banana";

console.log(clone);
// output: { name: 'Tom', age: 10, child: { name: 'banana' } }
// 可看到第一次克隆了,但第二层仍是引用。

真正的深层克隆,可用 JavaScript 库 lodash 中的 _.cloneDeep(obj) 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_ = require('lodash');

let user1 = {
name: "Tom",
age: 10,
child:{
name: "apple"
}
};

let clone = {};
clone = _.cloneDeep(user1);

user1.name = "Jimmy";
user1.child.name = "banana";

console.log(clone);
// output: 可看到修改原对象,并没影响到克隆的新对象:
// { name: 'Tom', age: 10, child: { name: 'apple' } }

对象方法

可以给对象创建方法,类似其它语言的 class method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let user = {
name: "Tom",
age: 22,
}

user.sayHello = function () {
console.log("hello");
}

user.sayHello();
// output:
// hello

// 也可以把已存在的方法赋值给函数方法:

hello = function () {
console.log("hello");
}

user.sayHello = hello;
user.sayHello();
// output:
// hello

类方法可写在类构造时:

1
2
3
4
5
6
7
8
9
10
11
let user = {
name: "Tom",
age: 22,
sayHello: function () {
console.log("hello " + this.name); // 在对象里,可通过 this引用属性。(不加this会报错)
}
}

user.sayHello();
// output:
// hello Tom

javascript 中的this,比其它语言中的this关键字更加宽范。他指定的内容是在运行时计算出来的。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const user = {name: "Tom"};
const admin = {name: "boss"};

function sayHello(){
console.log("hello " + this.name);
}

user.sayHi = sayHello;
admin.sayHi = sayHello;

user.sayHi();
admin.sayHi();
// output:
// hello Tom
// hello boss

this 并没有严格的与某个对象绑定。在javascript中,this比较自由。

注意:箭头头函数没有 this,如果在箭头函数中引用this,会取来自箭头函数外部的this,如:

1
2
3
4
5
6
7
8
9
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};

user.sayHi(); // Ilya

构造器 new

除了常规的 {…}来创建对象,也可以使用构造器。对于要多次创建的对象,用构造器更便捷。构造器函数实际上是个常规函数,有两个约定:

  1. 以大写字母开头
  2. 只能由 new 来执行

构造器执行时,由三个流程:

  1. 创建空对象{},并赋值给 this
  2. 执行函数体。
  3. 返回this

如:

1
2
3
4
5
6
7
8
9
10
function User(name) {
// this = {}
this.name = name;
this.role = "worker";
// return this
}

const u1 = new User("Tom");

console.log(u1); // User { name: 'Tom', role: 'worker' }

new 可以让任何函数以这种方式执行。但一般我们用于构建函数时,以大写字母命名,由new执行。

如果任何函数都可以被new来使用,那如何判断这个调用,是否是被new创建的呢?在函数内部,可使用 new.target 函数来测试是否是由new来创建的。如果不是new创建的,由返回undefined;否则,返回函数。

1
2
3
4
5
6
7
8
9
10
11
function User(name) {
if (!new.target) {
return new User(name);
}
this.name = name;
this.role = "worker";
}

const u1 = User("Tom");

console.log(u1); // User { name: 'Tom', role: 'worker' }

构造器的return

构造器一般无 return 语句

  • 如果构造器返回一个对象,则使用这个对象返回,而代替this
  • 如果构造器返回一个原始类型,则忽略。就像没这句一亲。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function User1(name) {
this.name = name;
return {name: "foo"}
}

function User2(name) {
this.name = name;
return "bar";
}
const u1 = new User1("Tom");
const u2 = new User2("Tom");

console.log(u1); // { name: 'foo' }
console.log(u2); // User2 { name: 'Tom' }

可选链

对于一个对象,我们可能会像这样的方式访问其属性值:user.address.city.name。但有可能其address未设置,而直接访问 user.address.city.name 就会报错,而我们想其返回undefined亦或null.

我们可以在user到name中,从根一步步的判断是否存在,存在再取下一步。这种方式可行,但代码太难看。

在javascript中有个可选链的操作,可优雅的达到此目的,使用方法为 value?.prop,他执行下面的操作:

  • 如果value为undefined或null,则返回undefined
  • 如果value存在,则返回 value.prop

如下:

1
2
3
4
5
6
7
8
9
10
const user = {
address: {
city: {
name: "beijing"
}
}
};

console.log(user?.addr?.city?.name); // undefined
console.log(user?.address?.city?.name); // beijing

可选链可用于函数和计算属性:

?.()或 ?.[]

也可用于删除属性:

delete user?.address

总之,可选链 ?. 语法有三种形式:

  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

Symbol

复习一下javascript中的原始类型,共有7种:

  • string
  • number
  • bigint
  • boolean
  • symbol
  • undefined
  • null

Symbol 的值是一个唯一的标识符,每次调用时不同。他在内部是不同的值,但在打印时,他的默认的 toString() 方法只展示其信息和id标识,并不显示唯一的标识信息值。如:

1
2
3
4
5
6
7
8
9
10
11
const id1 = Symbol("id");
const id2 = Symbol("id");

console.log(id1); // Symbol(id)
console.log(id1 == id2); // false

const id = Symbol("id desc");
console.log(id); // Symbol(id desc)
console.log(id.toString()); // Symbol(id desc)
console.log(id.description); // id desc

Symbol 的作用

Symbol 的最大作用,是允许我们创建对象的隐藏属性。比如我们引用第三方代码中的user对象,但我们想添加个标识符,又不想破坏原有代码,我们就可以起一个特殊的键来存放我们的属性值,而原有代码因不知道这个键,而不会感知到我们添加了内容。同时,Symbol对象在迭代时,会被忽略。

如:

1
2
3
4
5
6
7
8
let user = {     // 第三方代码中的数据
name: "Tom",
};

gender = Symbol("id");
user.gender = "male"; // 添加一个属性

console.log(user.gender); // male

在迭代时,Symbol对象会被忽略,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const id1 = Symbol("id1");

let user = {
name: "Tom",
[id1]: "333"
}

const id2 = Symbol("id2");
user[id2] = id2;

console.log(Object.keys(user)); // [ 'name' ]

for (let k in user) {
console.log(k); // name
}

当克隆一个对象时, Symbol 会被克隆:

1
2
3
4
5
6
7
8
9
let user = {
name: "Tom",
[Symbol("id")]: "333"
}

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

console.log(clone); // { name: 'Tom', [Symbol(id)]: '333' }

全局 symbol

系统中有个全局 Symbol 符号表。当通过 Symbol.for(key)来创建Symbol时,他会创建并返回一个Symbol对象,同时记录在全局符号表中。当符号表中存在同名的Symbol时,则会返回已存在的Symbol。

反之,也可通过 Symbol.keyFor(sym:symbol)来把Symbol转换为标识符。

1
2
3
4
5
6
7
8
9
let s1 = Symbol.for("id");
let s2 = Symbol.for("id");
let s3 = Symbol.for("id3");
console.log(s1 == s2); // true
console.log(s1 == s3); // false

const sss = Symbol("symbol not exists");
console.log(Symbol.keyFor(s1)); // id
console.log(Symbol.keyFor(sss)); // undefined

对象和原始值

JavaScript 允许我们像使用对象一样使用原始类型。

原始类型是一个原始值,就是7种原始类型中的任何一种的值。

对象,可以存储多个属性;可以使用{…}的方式创建且可以包含函数

原始类型,有很多的方法,而为了实现这些操作方法,内部通过包装器来实现。而包装器只在使用期间存在,用后即毁。如:

1
2
3
const str = "Hello world!";

console.log(str.toUpperCase());// HELLO WORLD!

在访问字符串的 toUpperCase方法时,

  1. str是个原始值。访问其方法,需要创建一个包装对象,此对象有其用到的方法
  2. 包装器的方法被执行,将返回值给调用者。
  3. 中间的包装器对象销毁

注意:原始类型的构造器仅供内部使用,自己的代码中不要使用,容易出现问题。而不在new的构造方法可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log(`typeof new Number(0): ${typeof new Number(0)}`);
console.log(`typeof Number(0): ${typeof Number(0)}`);

if (new Number(0)) {
console.log("new Number(0) is TRUE");
} else {
console.log("new Number(0) is false");
}

if ( Number(0)) {
console.log(" Number(0) is TRUE");
} else {
console.log(" Number(0) is false");
}
/*
typeof new Number(0): object
typeof Number(0): number
new Number(0) is TRUE
Number(0) is false
*/

undefined和null没包装器,也没任何方法。