JS基础 - 对象

288 阅读12分钟

对象类型是一种存储键值对(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')
  }
}

创建

  1. 对象字面量(Object Literal)
const user = {
  name: 'Klaus',
  age: 23
}
  1. new Object+动态添加属性
const user = new Object()
user.name = 'Klaus'
user.age = 23
  1. 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

  1. for key in Object | 数组 | 字符串 (set,map遍历不报错,但没结果)

    for value of Iterator (例如数组,字符串,set,map等,不包括原生对象)

  2. for-in会迭代原型上的属性,而for-of不会

堆内存和栈内存

程序的运行是需要加载到内存中来执行的,我们可以将内存划分为两个区域:栈内存和堆内存

  • 原始类型占据的空间是在栈内存中分配的
  • 对象类型占据的空间是在堆内存中分配的

值类型和引用类型

原始类型的保存方式:在变量中保存的是值本身, 所以原始类型也被称之为值类型

image.png

对象类型的保存方式:在变量中保存的是对象的“引用”(指针,地址),所以对象类型也被称之为引用类型

image.png

在赋值的时候,值类型传递的是值本身,而引用类型传递的是引用地址

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、李雷、韩梅梅是具体的对象

image.png

image.png

// 工厂函数创建
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的不同宿主环境被映射成对应宿主环境下的全局对象

全局对象的作用

  1. 在查找变量的时候,如果一直找不到,最终会在globalThis上进行查找

  2. 因为globalThis作为全局对象,可以用来被挂载全局变量,全局函数和全局对象

    例如: 在浏览器中console, alert, document等都会被挂载到全局对象window上

  3. 使用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 = '柴犬'