JavaScript基础—一文带你认识对象

前言

上一期说过了数组,这次分享一下我眼中的对象,在JavaScript中,这两个是很重要的,毕竟他们在你写的代码中随处可见,话不多说,正片开始。

一、创建对象的基础写法

说明:JavaScript中,大多数引用值的实例使用的是Object类型,它很适合存储和在应用程序之间来交换数据,显式的创建Object实例有两种方式,一种是使用new关键字和Object构造函数,一种是使用对象字面量

1.通过Object构造函数创建

说明: 对象通过new关键字后紧跟对象类型的名称来创建,创建之后就可以自己给对象添加属性或者是方法了

// 标准写法
let obj = new Object()

// 由于ECMAScript只要求给构造函数提供参数的时候需要使用括号,如果
// 没有参数,括号可以省略,也就得到下面这种简写的形式
let obj = new Object()
// 存值:
obj.name = 'zhangsan'

// 取值:
obj.name // 张三

这里需要注意的是:存值是可以往对象中添加不存在的属性,但是取值如果取到的属性不存在,那么取出来的值会是undefined

2.通过对象字面量的方式创建

说明: 对象字面量是对象定义的简写形式,目的是为了简化大量属性的对象的创建,并且使用此方法创建的对象是不会调用object构造函数的

let obj = {
    name: 'zhangsan'
}

3.对象中部分名词的解释

说明: 对象的本质是将一组变量和方法封装起来,便于你去使用,然后在对象中,变量被称为属性,函数被称为方法,一般创建对象使用最多的方式是使用字面量来创建,其中的数据是成对出现的,每一对称为键值对其中,键可以理解为变量名,但是变量名只允许是字符串类型,ECMAScript自动将对象的键设置为字符串类型,所以不需要手动加上引号值可以理解为变量所赋的值,值可以是任意类型,哪怕是对象类型也可以,键值对中间用:隔开,每对之间用隔开

// 这两种写法是等价的
let obj = {
  name: 'zhangsan',
  'name': 'zhangsan'  
}

console.log(obj.name) // zhangsan

关于取值,如果取值取的是属性,一种通过.操作符,这个.可以理解为的意思,另一种就是使用[]取值了,需要注意,[]取值里面的属性名需要使用字符串的表示形式,否则会报错,也就是[]可以通过变量来访问属性,如果取值是取方法,直接使用.取到方法,在后面加上一个(),这个方法就会被执行

let obj = {
  name: 'zhangsan',
  fn: () => {
    console.log(111)
  }
}

obj.fn() // 111
console.log(obj.name) // zhangsan
console.log(obj['name']) // zhangsan
// 如果属性名中包含非字母和数字的字符,就只能够通过[]来取值了
console.log(obj['first name'])

二、创建模式

说明: 虽然使用Object构造函数或者字面量可以很方便的创建对象,但是对于创建具有同样属性或方法的对象创建多个仍然会书写很多无用的代码,也就引申出下面的内容。

1.工厂模式

说明: 工厂模式提供了一种灵活的方式来统一管理和封装对象的创建过程,提高了代码的可维护性、扩展性和可复用性,同时也隐藏了具体的实现细节,降低了代码的耦合度。

function createPerson(name, age, job) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function () {
    console.log(this.name);
  };
  return o;
}

let person1 = createPerson("zhangsan", 29, "softWare");
let person2 = createPerson("lisi", 29, "doctor");

console.log(person1);
// {
//   name: 'zhangsan',
//   age: 29,
//   job: 'softWare',
//   sayName: [Function (anonymous)]
// }

console.log(person2);
// {
//   name: 'lisi',
//   age: 29,
//   job: 'doctor',
//   sayName: [Function (anonymous)]
// }

这种模式虽然能够通过改变参数的传递来构建多个类似对象,但是还是没有解决创建的对象是什么类型这个问题

2.构造函数模式

说明: ECMAScript中的构造函数是用于创建特定类型的对象的,像Object或者是Array这样的原生构造函数,运行时可以直接在执行环境中使用,当然也可以使用自定义构造函数,以函数的形式为自己的对象类型定义属性或者是方法

(1)基本使用

说明: 上面工厂模式的那个例子可以改写成下面这样

function CreatePerson(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    console.log(this.name);
  };
}

// 函数表达式也一样可以使用
let CreatePerson = function(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    console.log(this.name);
  };
}

let person1 = new CreatePerson("zhangsan", 29, "softWare");
let person2 = new CreatePerson("lisi", 29, "doctor");

console.log(person1);
// {
//   name: 'zhangsan',
//   age: 29,
//   job: 'softWare',
//   sayName: [Function (anonymous)]
// }

console.log(person2);
// {
//   name: 'lisi',
//   age: 29,
//   job: 'doctor',
//   sayName: [Function (anonymous)]
// }

区别:
1.没有显式地创建对象
2.属性和方法直接赋值给了this
3.没有return

注意:
1.构造函数名称的首字母需要大写,依次区分普通函数和构造函数

new关键字做的事:
1.在内存中创建一个空对象
2.将构造函数的[Prototype]特性赋值给空对象的[Prototype]特性
3.构造函数的this指向空对象
4.执行构造函数为空对象添加属性
5.如果构造函数返回非空对象,就返回该对象,否则返回新创建的对象

console.log(person1.constructor == CreatePerson); // true
console.log(person2.constructor == CreatePerson); // true

console.log(person1 instanceof CreatePerson); // true
console.log(person1 instanceof Object); // true

console.log(person2 instanceof CreatePerson); // true
console.log(person2 instanceof Object); // true

person1和person2分别保存着CreatePerson的不同实例,它们的constructor属性都会指向person,constructor用于标识对象类型,不过,一般使用instanceof来确定更加准确,

function CreatePerson() {
  this.name = 'zhangsan';
  this.sayName = function () {
    console.log(this.name);
  };
}

let person1 = new CreatePerson()
let person2 = new CreatePerson

person1.sayName() // zhangsan
person2.sayName() // zhangsan

在实例化的时候,如果不想传递参数,构造函数后面的()可加可不加,只要有new操作符,就可以调用相应的构造函数

(2)构造函数也是函数

说明: 构造函数与普通函数的区别在于调用方式上面,在调用的时候使用了new关键字就是构造函数,否则就是普通函数

// 以上面的person函数为例

// 作为构造函数使用
let person = new Person('zhangsan', 22, 'job')
person.sayName()

// 作为普通函数使用,属性和方法会添加到全局的window对象上面
Person('zhangsan', 22, 'job')
window.sayName()

值得注意的是: 如果调用了一个函数但是函数的this所指向的值不明确的时候,this始终指向全局对象,并且定义在构造函数上面的方法在每个实例上面都会重新的去执行一次

(3)构造函数使用的问题

// 在ECMAScript中,函数也是一种特殊的对象,那么前面的person函数在逻辑上
// 可以等价成下面这样的
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  // 这样每个person实例都有自己的Function实例用来显示name属性
  this.sayName = new Function(console.log(this.name));
}

但是这种创建的方式会代码不同的作用域链,虽然创建Function的机制是一样,不同实例上的函数虽然同名但是不相等

person1.sayName === person2.sayName // false

既然是做同一件事情那么就可以将其提取出来

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  // sayName属性等于全局的sayName(),这样多次创建实例就不会重复的去
  // 执行相同逻辑的内容了
  this.sayName = sayName;
}

// 由于这个函数定义在外面,那么这个函数就属于全局对象window的
function sayName() {
  console.log(this.name)
}

但是,如果Person函数存在很多套这种函数,那么就需要在window上定义很多种这样的方法,这就会导致自定义类型引用的代码不能够很好的聚集在一起

3.原型模式

(1)解释原型

说明: 可以将构造函数的原型想象成一个工厂,这个工厂负责生产对象实例所共享的属性和方法。当你在构造函数的原型上定义属性和方法时,相当于在这个工厂里面生产了一些产品(即属性和方法),而所有的对象实例都可以从这个工厂里取得相同的产品,如果将这些产品直接赋值给对象实例,那么每个实例都会有自己独立的产品,而这样会浪费很多空间。但是,如果将这些产品放到工厂(即原型)里面,所有的实例就可以共享这些产品,这样就能够节省内存空间,而且实例之间可以共享相同的产品,简化了维护和修改。

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例 共享的属性和方法,原来在构造函数中直接赋给对象实例的值,可以 直接赋值给它们的原型

function Person() {}

// 这里所有的属性和方法都添加到Person的原型上面,这样使这些属性和方法被所
// 有的实例共享,这也是为什么下面两个实例的sayName是相等的了
Person.prototype.name = 'zhangsan'
Person.prototype.age = 20
Person.prototype.sayName = function() {
    console.log(this.name)
}

let person1 = new Person()
let person2 = new Person()

person1.sayName() // 'zhangsan'
person2.sayName() // 'zhangsan'
console.log(person1.sayName() === person2.sayName()) // true

(2)理解原型

初解: 在创建一个函数的时候,这个函数会获得一个prototype属性,这个属性的值叫原型对象,它默认只有一个constructor属性指向与之关联的构造函数,所以,不同的构造函数,可能会有不同的属性和方法,也就得到原型对象会得到不同的属性和方法,同时,原型对象也是一个普通的对象,对象自身有自己的方法,而这些方法会继承在Object上面,当构造函数每创建一个实例,实例的内部[[Prototype]]就会被赋值构造函数的原型对象所替代

// 打印前面的构造函数Person的原型
console.log(Person.prototype)

// 得到这样的结果
{
    constructor: ƒ Person()
    [[Prototype]]: Object
}

constructor只存在于原型对象上面

// 前面说过原型对象,它默认只有一个constructor属性指向与之关联的构造函数,
// 也就得到:
console.log(Person.prototype.constructor === Person); // true

构造函数、原型对象和实例:

通过上面的分析可以得到这样的结论:

  • 实例通过__proto__链接到原型对象,它实际上指向隐藏特性[[Prototype]]
  • 构造函数通过 prototype 属性链接到原型对象
  • 实例与构造函数没有直接联系,与原型对象有直接联系
  • 同一个构造函数创建的两个实例共享同一个原型对象:
console.log(person1.__proto__ === Person.prototype); // true 
console.log(person1.__proto__.constructor === Person) // true 
console.log(person1.__proto__ === person2.__proto__); // true 

JavaScript基础---一文带你认识对象

(3)原型层级

说明: 在访问对象的属性的时候,会从这个对象本身开始寻找,如果找到了就返回对应属性名的属性值,如果没找到就去它的原型上面找,重复这样的操作,如果没找到就会返回undefined

原型对象上的值可以通过实例获取,但是不能通过实例更改,如果原型对象上面和实例上面都存在相同的属性,实例上的属性会遮住原型对象上的属性

function Person() {} 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
 console.log(this.name); 
}; 

let person1 = new Person(); 
let person2 = new Person(); 

// 由于实例和原型都存在属性name,根据查找规则,会从实例开刀,
// 在实例上面找到name后就不再向下了,这样实例上面的属性就会
// 遮蔽对原型上同名属性的访问,即使实例上属性的值为null,他也
// 不会改变遮蔽的行为
person1.name = "Greg"; 

console.log(person1.name); // "Greg",来自实例
console.log(person2.name); // "Nicholas",来自原型

* delete操作符

作用: 将指定的属性或元素从对象或数组中移除

// 将实例中的name属性删除后,这在在查找的时候就会查找到
// 原型上面的name属性并使用了,这样遮蔽的行为就失效了
delete person1.name; 
console.log(person1.name); // "Nicholas",来自原型

* hasOwnProperty方法

语法: obj.hasOwnProperty(propertyName),其中obj 是要检查的对象,propertyName 是要检查的属性名称

作用: 用于检查一个对象是否包含指定名称的属性,但是在查找的时候不会去原型对象上面查找(可以用来区分实例属性和原型属性)

返回值: 如果存在就会返回true,否则就是false

function Person() {} 

Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
    console.log(this.name); 
}; 

// 实例上没有name属性
let person1 = new Person(); 
let person2 = new Person(); 
console.log(person1.hasOwnProperty("name")); // false 

// 实例上有name属性
person1.name = "Greg"; 
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true 

// 实例上没有name属性
console.log(person2.name); // "Nicholas",来自原型
console.log(person2.hasOwnProperty("name")); // false 

// 实例上name属性被删除
delete person1.name; 
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false

(4)原型与循环

* in操作符

语法: propNameOrSymbol in objectpropNameOrSymbol是要检查的属性名称或者符号,object是要检查的对象

作用: 用于检查对象是否具有特定的属性,这个属性包括原型对象中的属性

返回值: 如果找到就返回true,否则就返回false

let person = {
  name: "Alice",
  age: 25,
};

console.log("name" in person); //  true
console.log("gender" in person); // false

* for-in中的in操作符

let person = {
  name: "Alice",
  age: 25,
};

for (let prop in person) {
  // 1.这里的prop就表示person中的属性,这样就可以用person[prop]来获取属性值了
  // 2.for-in 循环会遍历对象自身及其原型上的所有可枚举属性
  // 3.如果原型中存在不可枚举([[Enumerable]]特性被设置为 false)属性,
  //   但是在实例上遮蔽了这个属性,那么这个属性同样会出现在for-in循环里面
}

* Object.keys

语法: Object.keys(obj),其中,obj是要返回可枚举属性的对象

作用: 返回一个由对象自身可枚举的属性组成的数组,它不会遍历原型对象上面的属性,也就是得到实例属性

let person = {
  name: "Alice",
  age: 25,
};

let keys = Object.keys(person);
console.log(keys); // ["name", "age"]

* Object.getOwnPropertyNames

语法: Object.getOwnPropertyNames(obj),其中,obj是要返回属性名的对象

作用: 用于返回一个由对象自身所有属性名(包括可枚举和不可枚举)组成的数组。它不会遍历对象的原型对象上面的属性

let obj = {
  name: "Alice",
  age: 25
};

// 给 obj 对象添加一个不可枚举属性
Object.defineProperty(obj, 'gender', {
  value: 'female',
  enumerable: false
});

// 获取对象的所有属性名(包括不可枚举属性和可枚举属性)
let properties = Object.getOwnPropertyNames(obj);

console.log(properties) // ['name', 'age', 'gender']

* Object.getOwnPropertySymbols

语法: Object.getOwnPropertySymbols(obj),其中,obj是要返回带符号的属性名的对象

作用: 用于返回一个由对象自身所有属性名(包括可枚举和不可枚举)组成的数组,这个跟上面类似,只不过这个只返回符号的属性名而已,它同样不会遍历对象的原型对象上面的属性

let person = {
  [Symbol("name")]: "Alice",
  [Symbol("age")]: 25,
};

let symbols = Object.getOwnPropertySymbols(person);
console.log(symbols); // 输出: [Symbol(name), Symbol(age)]

(5)属性枚举顺序

说明: 枚举可以理解为遍历,for-in 循环Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()以及 Object.assign()在属性枚举顺序方面有很大区别,其顺序取决于浏览器的引擎,但是Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.assign()
的枚举顺序是确定性的,它的规则如下:

  • 首先将数字类型的键按照升序顺序进行遍历
  • 其次将字符串和符号类型的键按照出现在对象中的顺序进行遍历
  • 最后将上面两次的结果结合起来就得到最终的枚举结果了
let k1 = Symbol("k1"),
    k2 = Symbol("k2");
let o = {
  1: 1,
  first: "first",
  [k1]: "sym2",
  second: "second",
  0: 0,
};
o[k2] = "sym2";
o[3] = 3;
o.third = "third";
o[2] = 2;

// ['0', '1', '2', '3', 'first', 'second', 'third']
console.log(Object.getOwnPropertyNames(o)) 

(6)原型模式的问题

说明: 毕竟成也萧何,败也萧何,这个模式也不例外,它最大的缺点在于它共享的特性,这个明显体现在引用值的属性上面

function Person() {}

Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  // 这里friends的值是一个数组
  friends: ["Shelby", "Court"],
  sayName() {
    console.log(this.name);
  },
};

// 这里创建了两个实例
let person1 = new Person();
let person2 = new Person();

// 在实例person1的friends里面添加元素,由于引用的是地址,
// person1和person2都会收到影响
person1.friends.push("Van");

console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true

三、属性的介绍

1.属性的类型

说明: ECMA-262使用一些内部特性来描述属性,这些特性是为JavasSript实现引擎的规范定义的,那么开发者就不能够在JavaScript中直接访问这些属性,这里属性分为两种,一种是数据属性,一种是访问器属性,在此之前先来了解两个方法

(1)Object.defineProperty

说明: Object.defineProperty 是 JavaScript 中用于定义对象属性的方法。它允许我们精确地定义一个对象的属性,Object.defineProperty有三个参数,分别为obj 是要定义属性的对象,prop 是属性名,descriptor 是一个配置对象,这个对象存在五个属性可以选择用于定义属性的特性,分别是 valuewritableenumerableconfigurablegetset

value:属性的值
writable:属性是否可写(默认为 false
enumerable:属性是否可枚举(默认为 false
configurable:属性是否可配置(默认为 false
get:获取属性值的函数
set:设置属性值的函数

(2)Object.defineProperties

说明: 这个方法与上一个方法的区别在于这个方法可以一次性定义多个属性,而上一个方法只能定义一个,同时它的参数只有两个,一个是obj 是要定义属性的对象,descriptor 是一个配置对象,每一个属性是以属性名 : 配置对象的结构存在的,配置对象的参数构成与上一个的配置对象的构成是一致的,不过需要注意的是这个方法定义的属性是同时定义的

let book = { }

Object.defineProperties(book, {
  // 数据属性
  year_: {
    value: 1
  },

  // 数据属性
  edition: {
    value: 2
  },
  
  // 访问器属性
  year: {
    get() {
      return this.year_;
    },
    set(newValue) {
      if(newValue > 2017) {
        this.year_ = newValue;
        this.edition += newValue - 2017
      }
    },
  }
})

(3)数据属性

说明: 数据属性包含一个保存数据值的位置,值会从这个位置进行读取,也会写入到这个位置,如果使用Object.defineProperty来进行对象的设置,配置对象可选的属性只有前四个

let person = {
    name: 'zhangsan'
}

`这里显式给对象添加属性后,value的值会变成zhangsan,其他三个会被更改为true`

如果需要修改属性的默认属性,就必须去使用Object.defineProperty来更改

let person = {}
Object.defineProperty(person, 'name', {
  writable: false, // 值不可更改
  value: 'zhangsan'
})

console.log(person.name) // zhangsan
person.name = 'lisi'
console.log(person.name) // 由于属性值设置不可以更改,那么值还是zhangsan

在非严格模式下面重新给不可赋值的属性赋值的时候是赋不上去的,在严格模式下面则会报错

let person = {}
Object.defineProperty(person, 'name', {
  configurable: false, // 不可配置,这里理解为不可删除
  value: 'zhangsan'
})

console.log(person.name) // zhangsan
delete person.name
console.log(person.name) // 由于属性不可删除,所以操作无效,还是zhangsan

在非严格模式下面操作给不可配置的属性操作会失效的,在严格模式下面则会报错,其次,configurable一旦设置成false就是不可逆状态,此时就只可以修改writable的值,其他配置更改时都会报错

(4)访问器属性

说明: 这个属性不包含数据值,也就是配置选项中的value,也就不会存在值能不能够修改的问题了,也就是writable,但是它会包含一个get函数set函数,跟我之前说过的ES6入门中的存值器函数取值器函数是一样的,定义这两个函数时只可借助Object.defineProperty来实现

let book = {
  year_: 2017,
  edition: 1
}

Object.defineProperty(book, 'year', {
  // 可理解为取值器函数
  get() {
    return this.year_;
  },
  // 可以理解为存值器函数,newValue表示传递的新值
  set(newValue) {
    if(newValue > 2017) {
      this.year_ = newValue;
      this.edition += newValue - 2017
    }
  },
})

book.year = 2018
console.log(book.edition) // 2

对象中的_常用来表示该属性并不希望在对象方法的外部可以获取到,在定义访问器属性的时候,如果只存在get,表示这个属性是只读的,在非严格模式下面该操作会返回undefined,在严格模式下面则会报错,如果都存在,表示这个属性是可读可写的

2.属性的读取

说明: 如果属性可以通过方法进行设置的话,当然也会有方法来获取配置了什么配置项,获取的内容就是Object.defineProperty中第三个参数里面的内容,然后现在来看一下这两个方法

(1)Object.getOwnPropertyDescriptor

语法: Object.getOwnPropertyDescriptor(obj, prop)obj是要获取属性所存在的对象,prop是要获取指定属性描述的属性

作用: 获取指定对象上指定属性的属性特征

返回值: 一个对象,包含以下字段

  • value:属性的值
  • writable:表示属性值是否可以被修改
  • enumerable:表示属性是否可以被枚举
  • configurable:表示属性是否可以被删除或者修改属性描述符

注意: 这个方法只对实例属性有效,要对原型对象上属性使用时,直接在原型对象上面使用就可以了

var person = { name: "Alice", age: 25 };   
var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); 

// {
//     value: "Alice",
//     writable: true,
//     enumerable: true,
//     configurable: true
// }

(2)Object.getOwnPropertyDescriptors

说明: 这个方法就是在上个方法后面多一个s,也就是这个方法可以一次性取多个属性的配置项,它的参数只有一个,就是需要取的属性是那个对象里面的,返回值是一个对象,对象的结构和内容与上面的那个方法是一致的

let book = { }

Object.defineProperties(book, {
  year_: {
    value: 1
  },

  edition: {
    value: 2
  },
  
  year: {
    get() {
      return this.year_;
    },
    set(newValue) {
      if(newValue > 2017) {
        this.year_ = newValue;
        this.edition += newValue - 2017
      }
    },
  }
})

let test = Object.getOwnPropertyDescriptors(book)
// {
//   year_: {
//     value: 1, 
//     writable: false, 
//     enumerable: false, 
//     configurable: false
//   },

//   edition: { 
//     value: 2, 
//     writable: false, 
//     enumerable: false, 
//     configurable: false 
//   },

//   year: {
//     get: [Function: get],
//     set: [Function: set],
//     enumerable: false,
//     configurable: false
//   }
// }

console.log(test)

3.属性的简写

说明: 在给对象添加变量的时候,如果属性名和变量名是一致的,此时只使用变量名(不写冒号),就会解释成同名的属性键,如果没有找到同名的变量,就会报错,下面这两种写法的结果是等价的

// 通常写法:
let name = 'zhangsan'

let person = {
  name: name
}

console.log(person) // { name: 'zhangsan' }

// 简写形式
let NAME = 'zhangsan'

let PERSON = {
  NAME
}

console.log(PERSON) // { NAME: 'zhangsan' }

代码压缩程序会在不同的作用域之间保留属性名,防止找不到引用,即使参数的作用域只限定于函数作用域,编译器也会保留初始的变量名

function makePerson(name) {
  return {
    name
  }
}

function makePersonFirst(a) {
  return {
    name:a
  }
}

let person = makePerson('zhangsan')
let personFirst = makePersonFirst('zhangsan')

console.log(person.name); // zhangsan
console.log(personFirst.name); // zhangsan

4.可计算属性

说明: 在引入可计算属性之前,如果想使用变量的值作为属性,那么就必须先声明对象,然后再使用中括号语法来添加属性,也就是不能直接在对象字面量中直接动态命名属性

const nameKey = 'name'
const ageKey = 'age'
const jobKey = 'job'

let person = { }

person[nameKey] = 'zhangsan'
person[ageKey] = 27
person[jobKey] = 'SoftWare'

console.log(person); // { name: 'zhangsan', age: 27, job: 'SoftWare' }

可计算属性可以在对象字面量中完成动态属性赋值,中括号包围的对象属性键告诉运行时将其作为JavaScript的表达式而不是字符串来求值

const nameKey = "name";
const ageKey = "age";
const jobKey = "job";

let person = {
  [nameKey]: "zhangsan",
  [ageKey]: 27,
  [jobKey]: "SoftWare",
};

console.log(person); // { name: 'zhangsan', age: 27, job: 'SoftWare' }

因为被当做JavaScript表达式来求职,所以计算属性本身可以是很复杂的表达式,在实例化的过程中取值

const nameKey = "name";
const ageKey = "age";
const jobKey = "job";
let uniqueToken = 0;

function getUniqueKey(key) {
  return `${key}_${uniqueToken++}`;
}

let person = {
  [getUniqueKey(nameKey)]: "zhangsan",
  [getUniqueKey(ageKey)]: 27,
  [getUniqueKey(jobKey)]: "SoftWare",
};

console.log(person); // { name_0: 'zhangsan', age_1: 27, job_2: 'SoftWare' }

注意:
可计算属性表达式中抛出任何错误都会中断对象的创建,如果计算属性的表达式有副作用时,那就需要小心了,毕竟在完成计算之前是不可以回滚的

四、对象的操作

1.对象的合并

说明: 对象的合并就是将源对象中所有的属性复制到目标对象上,关于复制,后面可能会听到拷贝这种词语,拷贝其实分为两种,一种是浅拷贝,一种是深拷贝(关于拷贝后面再补充),它们是等价的,话不多说,看方法。

(1)Object.assign

说明:ECMAScript6中为对象的合并提供了Object.assign这个方法,它接受一个目标对象和一个或多个源对象作为参数,它会将每个源对象中可枚举,也就是可遍历(也就是这个属性如果他的enumerable配置选项是true)的属性和自有(Object.hasOwnProperty的返回值是true)的属性复制到目标对象上面去,对于每个符合条件的属性,它会调用源对象上面的get方法来取值,调用目标对象上面的set方法来存值

// 复制一个
let dest, src, result

dest = { }
src = {
  id: 'src'
}

result = Object.assign(dest, src)

// Object.assign会修改目标对象,也会将目标对象返回
console.log(dest === src) // false
console.log(dest === result) // true
console.log(dest) // { id: 'src' }
console.log(result) // { id: 'src' }
// 复制多个
let dest, src, result

dest = { }

result = Object.assign(dest, { a: 'haha' }, { b: 'hehe' })

console.log(result) // { a: 'haha', b: 'hehe' }
// 存值函数与取值函数
let dest, src, result

dest = { 
  set a(val) {
    console.log(val)
  }
}

src = { 
  get a() {
    return 'foo'
  }
}

// 这里会调用src的get方法进行取值,得到foo,
// 之后会调用dest方法将获取到的值foo作为参数
// 传递进来,但是这里不存在赋值的操作,所以并没有将值转移过来
result = Object.assign(dest, src) 

console.log(result) // { a: [Setter] }

Object.assign这个方法对每个源对象执行的是浅拷贝,如果存在相同的属性,是会被覆盖掉的

// 执行时属性相同会被覆盖
let dest, src, result

dest = { 
  id: 'foo'
}

// 可以通过目标对象上面的取值器来观察值覆盖的过程
dest = { 
  set id(val) {
    console.log(val)
    // haha
    // hehe
  }
}

result = Object.assign(dest, { id: 'haha' }, { id: 'hehe' })

console.log(result) // { id: 'hehe' }

Object.assign实际上是对每个源对象进行浅复制,如果多个源对象上面都会存在相同的属性,则会使用最后一个复制的值,并且从取值器获得的值会作为一个静态的值传递给目标对象,也就是说对象间不能通过这个方法进行存值函数和取值函数进行转移

// 由于是浅拷贝,那么只会复制引用的地址
let dest, src, result

dest = { }

src = {
  a: {}
}

Object.assign(dest, src)

console.log(src.a === dest.a) // true

当然,这个复制是会终止的,如果在复制的过程之中抛出错误,它会将复制的过程终止,但是终止前复制的内容还是存在的

let dest, src, result

dest = { }

src = {
  a: 'foo',
  get b() {
    // 在这个函数调用之前会抛出错误终止复制
    throw new Error()
  },
  c: 1
}

try {
  Object.assign(dest, src)
} catch(e) {

}

console.log(dest) // { a: 'foo' }

2.对象的相等判定

(1)Object.is

说明: Object.is() 方法是一种用于比较两个值是否严格相等的方法,它接受两个参数,其返回值是一个布尔值,用于判断是否相等,它与===的行为基本相同,但有些微小的差异。在以下情况下会将两个值视为相等,虽然区分正负零和 NaN,这在某些情况下可能是有用的。但在其他情况下,使用 === 和 !== 可能更合适。

  • 对于正零和负零,它们被认为是相等的。
  • 对于 NaN,它们被认为是相等的。注意,在使用 === 对两个 NaN 进行比较时,它们会被认为是不相等的。
  • 对于非零数值,如果它们具有相同的值和类型,则被认为是相等的。
// 正常情况下:
console.log(true === 1) // false
console.log({} === {}) // false
console.log('2' === 2) // false

// 在不同的js引擎中,判定的结果也不太一样,使用Object.is判定结果也不太一致
console.log(+0 === -0) // true
console.log(+0 === 0) // true
console.log(0 === -0) // true

// 要确定NaN的相等性,就需要使用isNaN()
console.log(NaN === NaN) // false
console.log(isNaN(NaN)) // true

// 使用Object.is来判断
console.log(Object.is(true, 1)) // false
console.log(Object.is({}, {})) // false
console.log(Object.is('2', 2)) // false

// 正确的判断NaN判定
console.log(Object.is(NaN, NaN)) // true

3.对象的解构

说明: ES6新增了对象的解构语法,可以在一条语句中使用嵌套数据实现一个或者多个赋值操作,简单来说,对象结构就是使用与对象匹配的结构来实现对象属性赋值

(1)基本使用

// 不使用对象解构
let person = {
  name: "zhangsan",
  age: 27,
};

let personName = person.name;
let personAge = person.age;

console.log(personName); // zhangsan
console.log(personAge); // 27
// 不使用对象解构
let person = {
  name: "zhangsan",
  age: 27,
};

let { name: personName, age: personAge } = person;

console.log(personName); // zhangsan
console.log(personAge); // 27

上面这两种写法是等价的,但还是很麻烦,其实解构可以像下面这样简写

// 简写形式
let person = {
  name: "zhangsan",
  age: 27,
};

let { name, age } = person;

console.log(name); // zhangsan
console.log(age); // 27

解构赋值不一定与对象的属性进行匹配,赋值的时候可以忽略某些属性,如果引用的属性不存在,那么该变量的值就是undefined

let person = {
  name: "zhangsan",
  age: 27,
};

let { name, job } = person;

console.log(name); // zhangsan
console.log(job); // undefined

为了避免解构的时候因为出现undefined而出现问题,可以使用默认值来解决,也就是如果解构的时候没有这个属性的话这个属性的值就会使用默认值

let person = {
  name: "zhangsan",
  age: 27,
};

let { name, job = 'software' } = person;

console.log(name); // zhangsan
console.log(job); // software

注意:
解构在内部使用函数ToObject()把源数据结构转换为对象,也就是说在解构的时候,原始值会被当做对象,同时null和undefined不能够被解构,否则会报错

ToObject():
它是 JavaScript 中的一种方法,它用于将一个值转换为 Object 类型。具体来说,如果传递给 ToObject() 方法的值是一个对象,则该方法不会产生任何影响。如果传递的值是基本类型,例如字符串、数字或布尔值,则 ToObject() 方法将创建一个相应类型的封装对象(比如 String、Number 和 Boolean 对象)并返回。如果传递给 ToObject 方法的值为 undefined 或 null,则会抛出 TypeError 错误。

let { length } = "zhangsan";
let { constructor: c } = 1;
let { _ } = null;
let { __ } = undefined;

console.log(length); // 8
console.log(c); // [Function: Number]
console.log(_); // 报错
console.log(__); // 报错

解构并不要求变量必须在解构表达式中声明,但是如果给事先声明好的变量赋值,赋值表达式必须在一个括号里面执行,因为它指示解构的是对象。如果省略花括号,解构赋值语法将会被解析为代码块而不是赋值语句

let personName, personAge;

let person = {
  name: "zhangsan",
  age: 27,
};

({ name: personName, age: personAge } = person);

console.log(personName); // zhangsan
console.log(personAge); // 27

(2)嵌套解构

解构对于引用的属性或者是赋值目标没有限制,所以可以通过解构来复制对象的属性,但是这种复制只是复制属性的引用而已

let person = {
  name: "zhangsan",
  age: 27,
  job: {
    title: "software",
  },
};

let personCopy = {};

({ name: personCopy.name,
   age: personCopy.age,
   job: personCopy.job
} = person);

console.log(personCopy); // { name: 'zhangsan', age: 27, job: { title: 'software' } }
// 由于复制的是属性的引用,那么更改一个的值,另一个也会跟着改变
person.job.title = "lisi";

console.log(person); // { name: 'zhangsan', age: 27, job: { title: 'lisi' } }
console.log(personCopy); // { name: 'zhangsan', age: 27, job: { title: 'lisi' } }

解构赋值可以使用与源对象相同的结构,以此来匹配嵌套的属性

let person = {
  name: "zhangsan",
  age: 27,
  job: {
    title: "software",
  },
};

let {
  job: { title },
} = person;

console.log(title); // software

在属性没有定义的情况下不能够使用嵌套解构,无论是源对象还是目标对象

let person = {
  job: {
    title: "software",
  },
};

let personCopy = {};

// foo在源对象上面是undefined
({
  foo: { bar: personCopy.bar },
} = person); // 会报错

// job在目标对象上面是undefined
({结构结构
  job: { title: personCopy.job.title },
} = person); // 会报错

(3)部分解构

如果存在多个属性的解构,其过程是无关顺序的,如果前面解构成功而后面出错,那么整个的解构只会执行一部分

let person = {
  name: "zhangsan",
  age: 24,
};

let personName, personAge, personBar;

try {
  ({
    // 没有foo这个属性
    name: personName,
    foo: { bar: personBar },
    age: personAge,
  } = person);
} catch (error) {}

console.log(personName, personAge, personBar); // zhangsan undefined undefined

(4)函数参数的解构

说明: 对参数的解构赋值不会影响arguments对象,但是可以在函数签名中声明在函数体的内部使用局部变量

let person = {
  name: "zhangsan",
  age: 24,
};

function fn(foo, { name, age }, bar) {
  console.log(arguments); // { '0': 1, '1': { name: 'zhangsan', age: 24 }, '2': 2 }
  console.log(name, age); // zhangsan 24
}

function fn1(foo, { name: personName, age: personAge }, bar) {
  console.log(arguments); // { '0': 1, '1': { name: 'zhangsan', age: 24 }, '2': 2 }
  console.log(personName, personAge); // zhangsan 24
}

fn(1, person, 2);
fn1(1, person, 2);

4.对象的迭代

(1)属性的迭代

* Object.values

语法: Object.values(obj),其中obj是需要操作的对象

作用: 用于提取一个对象的所有可枚举属性的(不包括继承的属性并且符号属性会忽略),并返回这些值组成的数组

const obj = { 
  a: 1, 
  b: 2,
  c: 3
};
const values = Object.values(obj);

console.log(values); // [1, 2, 3]

* Object.entries

语法: Object.entries(obj),其中obj是需要操作的对象

作用: 用于提取一个对象的所有可枚举属性的键值对(不包括继承的属性并且符号属性会忽略),并返回这些值组成的数组

转换后得到的属性会被强制转换成字符串类型

const obj = { 
  a: 1, 
  b: 2,
  c: 3
};
const entries = Object.entries(obj);

console.log(entries); // [["a", 1], ["b", 2], ["c", 3]]

(2)其它原型语法

有问题的版本:

// 假如需要给person原型上面整很多的属性,每整一个写一个person.propertype,
// 这样很繁琐,其实可以直接整一个对象将它们放一起,比如:

function Person() {}

Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName() {
    console.log(this.name);
  },
};

优化的版本:

// 上面的写法完全重写了默认的 prototype 对象,因此其 constructor 
// 属性也指向了完全不同的新对象(普通的对象的构造函数是Object),
// 不再指向原来的构造函数,如果constructor属性很重要,就需要在重写
// 原型对象的时候专门设置一个它的值,但是,普通的赋值话这个constructor属性
// 是可枚举的,但是这个属性默认是不可枚举的,所以可以按照下面的做法来更改:

function Person() {}

Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName() {
    console.log(this.name);
  },
};

// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person,
});

* instanceof运算符

语法: object instanceof classobject是要检查的对象,class是要检查的类名或构造函数名

作用: 用于检查一个对象是否是某个特定类的实例,或者是该类的派生类的实例

返回值: 如果满足返回true,否则返回false

// 以上面的Person函数为例,由于它被修改了原型对象,
// 那么此时friend这个实例的构造函数就变成了Object,
// 因此下面两条语句执行返回都是true

let friend = new Person(); 

console.log(friend instanceof Object); // true 
console.log(friend instanceof Person); // true 

(3)原型的动态性

说明: 从原型上面搜索值的过程是动态的,任何时候对原型对象所做的修改也会在实例上反映出来

let friend = new Person(); 

Person.prototype.sayHi = function() { 
 console.log("hi"); 
}; 

friend.sayHi(); // hi

当你完全重写一个构造函数的原型时,已经存在的实例仍然引用着最初的原型,而不是新的重写后的原型

function Person() {}

let friend = new Person();

Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName() {
    console.log(this.name);
  },
};

friend.sayName(); // 报错

五、继承

1.原型链

看图:

JavaScript基础---一文带你认识对象

每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味 着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链

简单的原型链:

// 构造函数SuperType上存在一个属性property
function SuperType() {
  this.property = true;
}

// 之后在其原型上面添加一个方法
SuperType.prototype.getSuperValue = function () {
  return this.property;
};

// 构造函数SubType上面存在一个属性subproperty
function SubType() {
  this.subproperty = false;
}

// 之后在其原型上面添加一个方法
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

// 此时SubType的原型被SuperType的实例所替换,那么SubType就可以访问SuperType所有的属性和方法
SubType.prototype = new SuperType();

let instance = new SubType();

// 此时创造出来的SubType实例访问SuperType的方法是不会报错的
console.log(instance.getSuperValue()); // true

在存在原型链的时候,原型的搜索机制会按照实例、实例原型、实例原型的原型的顺序查找,直到原型链的顶端Object

(1)检测原型关系

说明: 这里介绍两个,另一个是instanceof(上面介绍过)

* isPrototypeOf

语法: prototypeObject.isPrototypeOf(object),其中 prototypeObject 是要检查的原型对象object 是 要检查的对象

作用: 用于检查一个对象是否是另一个对象的原型

返回值: 如果是就是true,否则就是false

function SuperType() {
  this.property = true;
}

let instance = new SuperType();

console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(Object.prototype.isPrototypeOf(instance)); // true

(2)对于方法

说明: 子类可以存在遮蔽父类的方法的行为,也可以存在添加父类没有的方法的行为,不过这些操作需要在原型赋值之后再添加到原型上面

以上面原型链为例:

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

SubType.prototype = new SuperType();

// SubType自己独有的方法
SubType.prototype.getSubValue = function ()c {
  return this.subproperty;
};

// 此时 SubType 上面的 继承的 getSuperValue 方法会被下面的操作所遮蔽,所以在执行时其返回值是false
SubType.prototype.getSuperValue = function () {
  return false;
};

let instance = new SubType();

console.log(instance.getSuperValue()); // false

注意: 以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链(4.4.3中有提到)

(3)问题

  • 原型的引用值会在所有的实例之间共享
  • 子类型在实例化的时候无法给父类型的构造函数传递参数

2.盗用构造函数

说明: 通常使用callapply来在子构造函数里面执行父构造函数,以此获取父构造函数中的属性和方法,这是为了解决原型链中引用值共用的问题

function SuperType(name) {
  this.colors = ["red", "blue", "green"];
  this.name = name;
}

function SubType() {
  // 调用父构造函数,并以子类的实例对象作为调用上下文,
  // 这样子类就可以获取父构造函数的逻辑,以此完成对父的继承
  SuperType.call(this, "zhangsan");

  this.age = 29;
}

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
console.log(instance1.name); // "zhangsan"
console.log(instance1.age); // 29

let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"

优点:

  • 子类构造函数可以向父类构造函数传参

缺点:

  • 父构造函数不能调用多次产多个实例(只执行一次)
  • 构造函数原型上的方法无法共享

3.组合继承

说明: 这种继承使用原型链继承方法构造函数继承属性结合所得,有点取其精华的味道

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  // 继承属性
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
  console.log(this.age);
};

let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27

4.原型式继承

说明: 其理念是即使不自定义类型也可以通过原型实现对象之间的信息共享,简单点就是浅复制,也可以通过Object.create来实现

核心: 对传入的对象执行了一次浅复制

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

* Object.create

作用: 用于创建一个新对象,并可以指定该对象的原型

语法: Object.create(proto, [propertiesObject])proto表示一个原型对象,必要的propertiesObject表示一个可选的配置对象,用于配置前面的原型对象中每个属性的相关配置

propertiesObject 的相关配置:

  • configurable:是否可配置,即该属性是否可以通过 delete 删除,并且是否可以再次修改属性描述符。默认为 false
  • enumerable:是否可枚举,即该属性是否会出现在对象的枚举属性中。默认为 false
  • value:属性的值,默认为 undefined,值得注意的是,这种方式添加的属性会遮蔽原型上的同名属性
  • writable:是否可写,即该属性的值是否可以被赋值运算符改变。默认为 false
  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined
let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

let anotherPerson = Object.create(person, {
  name: {
    value: "Greg",
  },
});

console.log(anotherPerson.name); // "Greg"
  • 原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。
  • 属性中包含的引用值始终会在相关对象间共享

5.寄生式继承

说明: 创建一个用于封装增强行为的函数,然后返回一个新对象,该对象继承了指定对象的属性和方法。

// 父对象
var person = {
  name: "Alice",
  sayHello: function () {
    console.log("Hello, my name is " + this.name);
  },
};

// 寄生式继承
function createStudent(person, grade) {
  var student = Object.create(person); // 使用 Object.create 创建新对象,继承 person 对象
  student.grade = grade; // 增加对象的属性
  student.sayGrade = function () {
    // 增加对象的方法
    console.log("I am in grade " + this.grade);
  };
  return student; // 返回新对象
}

// 创建一个 student 对象
var student = createStudent(person, 8);
console.log(student.name); // 输出: "Alice"
console.log(student.grade); // 输出: 8
student.sayHello(); // 输出: "Hello, my name is Alice"
student.sayGrade(); // 输出: "I am in grade 8"

通过这样的寄生式继承方式,我们扩展了 person 对象,创建了一个新的对象 student,该对象继承了 person 对象的属性和方法,并增加了自身的属性和方法。这样,我们可以实现一种基于已有对象构建新对象的方式,并在新对象上添加或改变特定的属性和方法,对于创建新对象,任何返回新对象的方法都可以使用最后就是寄生式继承给对象添加函数会导致函数难以重用,需要注意

6.寄生式组合继承

说明: 由于组合继承会将父类构造函数执行两次,一次原型链继承时给子类构造函数实例,一次盗用构造函数在子类构造函数内执行获取父类构造函数的逻辑,其实,只需子类构造函数只要在执行时重写自己的原型就行了,所以使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型来解决

function inheritPrototype(subType, superType) {
  // 创建对象(创建父类原型的一个副本)
  let prototype = object(superType.prototype); 
  
  // 增强对象(解决由于重写原型导致默认 constructor 丢失的问题)
  prototype.constructor = subType; 
  
  // 赋值对象
  subType.prototype = prototype; 
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

// 只调用一次父类构造函数,避免子类构造函数存在不必要用到的属性
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
  console.log(this.age);
};

原文链接:https://juejin.cn/post/7337169269950447651 作者:NGC_2237

(0)
上一篇 2024年2月20日 上午10:15
下一篇 2024年2月20日 上午10:26

相关推荐

发表评论

登录后才能评论