对象类型是一种存储键值对(key-value)的复杂的数据类型
基本数据类型可以存储一些简单的值,但是现实世界的事物抽象成程序时,往往比较复杂
这个时候,我们需要一种新的类型将这些特性和行为组织在一起,这种类型就是对象类型
对象类型可以使用{...}来创建的复杂类型,里面包含的是键值对(“key: value”)
// 对象的多个键值对之间使用逗号进行分割
const firend = {
// name是属性名
// Klaus是属性值
name: 'Klaus',
// 对象的属性名可以是字符串或symbol类型的值
// 如果key是字符串类型,且是合法的js变量对应的字符串的时候,key的引号一般会省略
age: 23,
// 但是如果key的类型是字符串,且key不是一个合法的js变量的时候,key的引号不可以省略
'firend-name': 'Alex',
// 对象的值可以是任何的合法数据类型
// 如果对应的属性值是一个函数(function)的时候,我们就称这种函数为方法(method)
run() {
console.log('running')
}
}
创建
- 对象字面量(Object Literal)
const user = {
name: 'Klaus',
age: 23
}
- new Object+动态添加属性
const user = new Object()
user.name = 'Klaus'
user.age = 23
- new 其他类 (可以是JS内置的类也可以是用户自定义的类)
function Person(name, age) {
this.name = name
this.age = age
}
const per = new Person('Klaus', 23)
常见操作
const user = {
name: 'Klaus',
age: 23,
running() {
console.log('running')
}
}
// 访问属性
// 如果访问一个对象上没有定义的属性的时候
// 那么获取到的属性值就是undefined
console.log(user.name)
user.running()
// 修改属性
user.age = 18
// 新增属性
user.height = 1.88
// 删除属性
// 使用delete关键字(操作符)
delete user.height
访问对象点语法和中括号语法
方括号在对象中的使用,使我们在定义或者操作属性时更加的灵活
let address = Symbol('address')
let key = 'age'
const user = {
// 在对象中,如果key的类型不是字符串或Symbol类型值的时候
// js会尽可能将key所对应的值转换为字符串类型 如 1 -> '1', true -> 'true'
name: 'Klaus',
age: 23,
'friend-name': 'Alex',
// 计算属性: 属性值使用方括号进行包裹
// 方括号中的值 可以是变量, js表达式 或 普通变量
[address]: 'shanghai'
}
// 点的方式去访问对象的属性
// 这个属性必须是一个合法的JS变量
console.log(user.name)
// 也可以使用中括号方式去访问对象的属性
// 此时这个属性名可以任意,可以是变量,合法的js变量对应的字符串,或是其它
console.log(user['name']) // 合法js变量对应的字符串
console.log(user['friend-name']) // 不合法js变量对应的字符串
console.log(user[key]) // 普通变量
console.log(user[address]) // symbol类型的变量
遍历
对象的遍历(迭代):表示获取对象中所有的属性和方法
注意: 以下方式进行遍历只能遍历自身的可枚举属性,不可枚举属性和定义在原型中的属性和方法是无法获取的
| 方法名 | 功能 |
|---|---|
| Object.keys() | 给定对象的自身可枚举属性key组成的数组 |
| Object.values() | 给定对象的自身可枚举属性value组成的数组 |
| Object.entries() | 给定对象的自身可枚举属性key和value组成的二维数组 |
const user = {
name: 'Klaus',
age: 23,
firend: 'Alex'
}
console.log(Object.keys(user)) // => [ 'name', 'age', 'firend' ]
console.log(Object.values(user)) // => [ 'Klaus', 23, 'Alex' ]
console.log(Object.entries(user)) // => [ [ 'name', 'Klaus' ], [ 'age', 23 ], [ 'firend', 'Alex' ] ]
遍历对象 --- 普通for循环
const user = {
name: 'Klaus',
age: 23
}
const keys = Object.keys(user)
for (let i = 0; i < keys.length; i++) {
console.log(`key: ${keys[i]}`)
console.log(`value: ${user[keys[i]]}`)
}
遍历对象 --- for - in
const user = {
name: 'Klaus',
age: 23
}
for (const key in user) {
console.log(key, user[key])
}
for-in vs for-of
-
for key in Object | 数组 | 字符串 (set,map遍历不报错,但没结果)
for value of Iterator (例如数组,字符串,set,map等,不包括原生对象)
-
for-in会迭代原型上的属性,而for-of不会
堆内存和栈内存
程序的运行是需要加载到内存中来执行的,我们可以将内存划分为两个区域:栈内存和堆内存
- 原始类型占据的空间是在栈内存中分配的
- 对象类型占据的空间是在堆内存中分配的
值类型和引用类型
原始类型的保存方式:在变量中保存的是值本身, 所以原始类型也被称之为值类型
对象类型的保存方式:在变量中保存的是对象的“引用”(指针,地址),所以对象类型也被称之为引用类型
在赋值的时候,值类型传递的是值本身,而引用类型传递的是引用地址
const user = {
name: 'Klaus'
}
function foo(obj) {
// 没有修改引用中的变量值
// 而是直接修改了整个变量的引用地址
obj = {
name: 'Alex'
}
}
foo(user)
console.log(user) // => { name: 'Klaus' }
const user = {
name: 'Klaus'
}
function foo(obj) {
// 修改了引用地址所对应的对象中的属性值
obj.name = 'Alex'
}
foo(user)
console.log(user) // => { name: 'Alex' }
在JS中,每定义一个对象,就会在堆中新建一个内存空间来进行存储
const obj1 = {}
const obj2 = {}
console.log(obj1 === obj2) // => false
批量创建对象
在开发中经常需要创建一系列的相似的对象,如果我们一个个都通过字面量方式手动进行创建,必然会十分的麻烦,而且存在大量的重复性代码
// 在这里的属性key都是重复的,不同的仅仅是属性值
const user1 = {
name: 'Klaus',
age: 23
}
const user2 = {
name: 'Alex',
age: 18
}
工厂函数
为了我们可以便于我们批量创建相似对象,我们可以使用工厂函数
// 工厂函数创建
function createUser(name, age) {
let user = {}
user.name = name
user.age = age
return user
}
// user1 和 user2 的类型是Object
const user1 = createUser('Klaus', 23)
const user2 = createUser('Alex', 24)
但工厂函数存在一个比较大的问题,工厂函数内部其实是使用new Obejct()来创建我们对于的实例对象
所以使用工厂函数返回的对象的类型都是Obejct类型
但是我们更希望的时候可以细分不同实例对象的类型,例如学生实例都是Student类型的,水果实例都是Fruit类型的
为此JavaScript为我们提供了一种更为简便的方式去批量创建相似对象,那就是构造函数
构造函数
构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数
在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法
但是JavaScript中的构造函数有点不太一样,构造函数扮演了其他语言中类的角色
也就是在JavaScript中,构造函数其实就是其它编程语言中的类
在ES5之前,我们都是通过function来声明一个构造函数(类)的,之后通过new关键字来对其进行调用
在ES6之后,JavaScript可以像别的语言一样,通过class来声明一个类
构造函数和类之间的关系
现实生活中往往是根据一份描述/一个模板来描述一个对象的
编程语言也是一样, 也必须先有一份描述, 在这份描述中说明将来创建出来的对象有哪些属性(成员变量)和行为(成员方法)
其实就是将多个对象中共有的属性和方法进行抽取(抽象),形成一个对于相似对象的具体描述信息
而这份描述就被称之为类,所描述的对象一般就被称之为实例对象
比如水果fruits是一类事物的统称,苹果、橘子、葡萄等是具体的对象
比如人person是一类事物的统称,而Jim、Lucy、Lily、李雷、韩梅梅是具体的对象
// 工厂函数创建
function createUser(name, age) {
let user = {}
user.name = name
user.age = age
return user
}
// user1 和 user2 的类型是Object
const user1 = createUser('Klaus', 23)
const user2 = createUser('Alex', 24)
// 构造函数创建
// 构造函数本质就是一个特殊的函数, 是JavaScript中专门用于构造实例对象的函数
// 在ES5中, 构造函数既可以通过new关键字进行调用,也可以和普通函数一样进行调用(这种调用方式对构造函数而言没有意义)
// 因此我们一般使用大驼峰命名法对构造函数进行命名,以便于区分构造函数和普通函数
function User(name, age) {
this.name = name
this.age = age
}
// 当一个构造函数使用new关键字进行创建的时候,会自动创建一个空对象,并将构造函数中的this指向该对象(就是this = {})
// 也就是我们可以省略工厂函数中的变量初始化语句,即let user = {}
// 如果构造函数返回的不是一个对象,那么构造函数会自动将创建的那个对象,也就是this所执行的那个对象返回
// 也就是我们可以省略工厂函数中返回变量语句,即return user
// 所以构造函数创建对象本质就是JavaScript提供了一种更为便捷的,更符合面向对象编程思维的创建对象方式
// 且使用构造函数创建类型的时候,JS会将所创建的实例对象的类型修改为构造函数的名称,因此我们可以得到更为具体的实例类型
// user3 和 user4 的类型是 User
const user3 = new User('Klaus', 23)
const user4 = new User('Alex', 24)
// 构造函数也可以当做普通函数进行调用,但这样调用没有意义
function User(username, age) {
this.username = username
this.age = age
}
console.log(User('Klaus', 34)) // => undefined
// 默认调用下,this指向globalThis,所以username和age会被挂载到GO上
// 但是在严格模式下,this无法指向globalThis,其值是undefined, 因此会报错
console.log(username, age)
function User(username, age) {
this.username = username
this.age = age
// 如果构造函数没有返回对象类型的值,那么将默认将this所指向的对象,也就是创建出来的实例对象进行返回
// 如果构造函数返回了对象类型的值,那么就直接返回对象类型的值,不在返回创建出来的实例对象
return {}
}
const user = new User('Klaus', 23)
console.log(user) // => {}
在JavaScript中类的表示形式就是构造函数
- 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别
- 如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数
如果一个函数被使用new操作符调用了,那么它会执行如下操作:
- 在内存中创建一个新的对象(空对象)
- 这个对象内部的[[prototype]]属性(也就是
__proto__属性)会被赋值为该构造函数的prototype属性 - 构造函数内部的this,会指向创建出来的新对象
- 执行函数的内部代码(函数体代码)
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
补充
globalThis
在JavaScript的宿主环境(宿主环境又叫运行环境)中,会提供一个全局对象
在浏览器中,这个对象就是window镀锡
在node中,这个对象就是global
在web worker中,这个对象就是self
所以ECMA提供了一个特殊的全局变量就在globalThis
该变量会根据JavaScript的不同宿主环境被映射成对应宿主环境下的全局对象
全局对象的作用
-
在查找变量的时候,如果一直找不到,最终会在globalThis上进行查找
-
因为globalThis作为全局对象,可以用来被挂载全局变量,全局函数和全局对象
例如: 在浏览器中console, alert, document等都会被挂载到全局对象window上
-
使用var定义的变量或隐式全局变量默认会被自动挂载到globalThis上 --- ES6开始已经不被推荐
globalThis作为一个对象,在这个对象中有一个特殊的属性叫做globalThis,指向其自身
所以我们可以使用 globalThis.globalThis.glpbalThis.... 进行无限调用
最终的结果都是指向globalThis自身
函数是一个特殊的对象
在JavaScript中,函数是一种特殊的可以执行的对象
所以当我们创建一个函数的时候,对应的函数代码会被存放到堆内存中
在执行的时候,会在栈中开辟对应的函数执行上下文,执行对应的函数
// 我们知道使用对象字面量的形式创建函数的本质是使用new Object来创建对象
const obj = {} // 等价于 const obj = new Object()
// 而使用function创建函数其实也是一种特殊的字面量写法
// 其本质也是使用new Function来进行创建的
const foo = function() {
console.log('hello world')
}
// 所以function本质上也是对象的一种
// 又因为 在JavaScript中Object是一切对象的父类
// 所以 Function其实是Object的子类(也就是说 Function extends Object)
// Function的定义方式为 new Function(...args, functionBody)
// 使用new Function的形式来创建函数是十分不方便的
// 所以在实际开发中,一般都是使用function关键字来进行创建
// 最后一个参数为字符串形式表示的函数体
// 之前的参数为函数所需要使用的实参,以可变参数的形式依次进行传入
const sum = new Function('a', 'b', 'return a + b');
const foo = new Function('console.log("hello world")')
console.log(sum(2, 6)); // => 8
foo() // => hello world
函数既然是对象的一种,那么我们也可以使用函数的属性或给函数添加对应的属性
function fun() {}
// 函数上有一个特殊的属性name,其值为函数名
console.log(fun.name) // => 'fun'
function Dog() {}
// Dog实例进行调用的方法被称之为 实例方法
Dog.prototype.running = () => console.log('running')
// Dog实例使用的属性被称之为 实例属性
Dog.prototype.name = '大黄'
// 挂载到构造函数上的方法 被称之为类方法
Dog.playing = () => console.log('playing')
// 挂载(添加)到构造函数上的属性 被称之为类属性
Dog.bread = '柴犬'