JavaScript中的对象【面试必备】

216 阅读7分钟

前言

面向对象语言都有类的概念,但是ECMAScript没有,因此他的对象同其他OO语言的对象也有所不同;

对象即一组无序属性的集合,每个属性都有一个名字(字符串),每个名字都映射一个值,这个值可以是普通值、对象和函数;我们把可以用对象存储哈希表。

ECMAScript的两种属性

1.数据属性

每一个数据属性都包含有四个特征属性:

  • [[Configurable]]:boolean,表示能否使用DELETE删除,能否修改属性为访问器属性,默认为true

  • [[Enumerable]]:boolean,表示能否通过for-in等循环返回属性,是否可枚举,默认为true

  • [[Writable]]:boolean,表示能否修改属性的值,默认为true

  • [[Value]]:any,数据的值,默认为undefined

修改特征属性必须通过Object.defineProperty():

const Person = {};

Object.defineProperty(person, name, {
  value: 'shuai',
  writable: false
});

console.log(Person.name); // shuai
Person.name = 'baijian'; // 忽略或者报错

注意:可以通过Object.defineProperty()对同一个属性的特性进行多次修改,但若把[[Configurable]]设置为false后,就不能修改除了[[Writable]]之外的特性了;使用Object.defineProperty()定义属性若不显示的指明三个布尔值特性的值默认将为false,如上的例子,Person的name属性的[[Configurable]][[Enumerable]]为false,我们使用for-in遍历不出Person的name属性。

2.访问器属性

访问器属性也有[[Configurable]]和[[Enumerable]]特性,功能与数据属性一样;访问器属性用[[Get]]特性来代替[[Value]]特性性获取属性值,用[[Set]]特性来代替[[Writable]]特性确定该属性是否可赋值;

  • [[Get]]:Function,读取属性时执行的函数,默认undefined

  • [[Set]]:Function,设置属性值时调用的函数,默认undefined

设置访问器属性的特性一样 要通过Object.defineProperty():

const Person = {
  name: 'shuai',
  _age: 23,
  edition: 1
};

Object.defineProperty(Person, age, {
  get: function() {
    return this._age;
  },
  set: function(newVal) {
    this._age = newVal;
    this.edition++;
  }
});

Persion.age = 24;
console.log(Persion.edition); // 2
console.log(Persion.age); // 24

获取属性的特性可以通过Object.getOwnPropertyDescriptor()方法,接受两个参数,第一个是对象,第二个是需要获取特性的属性名:

const Person = {
  name: "shuai",
  _age: 22,
  edition: 1
};

Object.getOwnPropertyDescriptor(person, 'name');
// {value: 'shuai', writable: true, enumerable: true, configurable: true}

创建对象的方式

工厂方式

工厂方式即用一个函数来封装构建函数细节,函数返回一个对象,构造函数对象时调用这个函数即可,如下:

function createObj(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  return o;
}

const person = createObj('shuai', 23, 'programmer');

  • 优点:创建多个相似对象只需调用一个函数

  • 缺点:无法判断该对象的类型

构造函数模式

function Person (name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;

  this.sayhi = () => {
    console.log('hi,' + this.name);
  }
}

const person = new Person('shuai', 23, 'programmer');

构造函数模式与工厂函数相比,内部没有显示声明一个对象,也没有返回值,且在调用时必须使用new关键字;

调用一个构造函数来创建对象需经历以下四步:

  1. new关键字声明

  2. 将构造函数作用域赋给新对象

  3. 执行构造函数里的代码

  4. 返回新对象

我们可以通过instance来判断通过构造函数创建的对象的类型:

console.log(person instance of Person); // true
console.log(person instance of Object); // true

在Person构造函数中,内部定义了一个sayhi方法,如果我们每通过new Person去创建一个对象,那么我们就会在对象内部新建一个sayhi方法,sayhi函数实现是一模一样的,但是却被多次声明,因此我们可以在构造函数外部声明一个函数,在内部指向该函数,如下:

function Person (name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;

  this.sayHi = sayHi;
}

function sayHi () {
  console.log("hi!" + this.name);
}

因为函数名是一个指针,我们把sayhi赋值给this.sayhi实际上就是把sayhi函数的地址给了this.sayhi,每新建一个对象只是对该函数新增了一个引用,每个对象调用的sayhi()都是指向的同一个函数,调用sayhi()时里面的this会指向调用它的对象;

这样虽然解决了创建多个相同函数的问题,但是又新增了问题,就是当对象的方法很多时,就要在全局作用域新建许多的方法,这样就破坏了自定义的引用类型的封装性。

  • 优点:能够判断对象的类型

  • 缺点:重复创建相同函数或者破坏了自定义引用类型的封装性

原型模式

每个函数都有一个原型对象,因此我们可以在函数的原型对象上建属性和方法,这样调用这个构造函数创建的对象就能共享该函数原型上的实例和方法了,如:

function Person () {};

Person.prototype.name = "chenlei";
Person.prototype.age = 22;
Person.prototype.job = "student";

Person.prototype.sayHi = function () {
  console.log("hi!" + this.name);
}

var person1 = new Person();

我们用原型模式构造一个对象时不用传入参数,所有对象实例共享原型上的属性和方法,这样就避免了我们在全局作用域定义的方法来供所有实例对象使用,但是当原型上的属性存在用用类型值时,就显得不那么方便了,如:

function Person () {};

Person.prototype.name = "chenlei";
Person.prototype.age = 22;
Person.prototype.job = "student";
Person.prototype.friends = ['chenjunbin', 'linweiming', 'zenpeisen'];

Person.prototype.sayHi = function () {
  console.log("hi!" + this.name);
}

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

person1.friends.push("lanlan");
person2.friends; // "chenjunbin","linweiming","zenpeisen","lanlan"

可以看到,person1.friends的变化会影响到person2,我们也可以给person2的friends重新赋一个值来屏蔽原型上的值,但这也就失去了我们使用原型模式构造对象的意义,就是实现共享。

  • 优点:让所有实例对象共享原型上的属性和方法

  • 缺点:当原型上的属性存在引用类型值时显得不那么方便

组合使用构造函数和原型模式

这个方法其实就是将构造函数和原型模式两种方式结合起来,把对象实例的属性在构造函数里定义,共享的属性和方法在原型上定义,如:

function Person (name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;

  this.friends = ['chenjunbin', 'linweiming', 'zenpeisen'];
}

Person.prototype.sayHi = function () {
  console.log("hi!" + this.name);
}

这样,friends属性除了初始值外可以往里面添加,而且各个实例互不影响,而且它们也都共享原型上的sayHi方法;这种方法是目前ECMAScript使用最广泛的自定义类型方法。

寄生构造函数模式

模拟修改原生的构造函数,举个例子,我们声明一个数组可以用new Array(),Array就是原生提供的构造函数,在其prototype上定义了很多方法,如splice,join等,但是我们不能给Array新增自定义方法,我们只能构造另一个函数寄生与Array,如下:

function MyArray () {
  var arr = new Array();
  arr.push.apply(arr, arguments);
  arr.toPipedString = function () {
    return this.join("|");
  }
  return arr;
}

var arr1 = new MyArray("1","2","3");
console.log(arr1.toPipedString); //"1|2|3"

ES6新增class

文章开篇提到过在ECMAScript中是不存在“类”的概念的,ES6中提供class关键字来定义一个类使之更加接近其他OO语言,使用class来声明一个对象的基本写法如下:

class Person {
  constructor (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
  }

  sayHi () {
    console.log(this.name);
  } 
}

var person1 = new Person("shuai", 23, "programmer");

由上我们通过class来重构之前的例子,我们要创建一个实例对象同样要使用new操作符,这种方法不仅是语法上更接近其他OO语言,还把定义一个对象的方法封装在一起了;

上面的constructor就是我们使用new操作符其作用的地方,class类里面的所有方法都是定义在它的原型对象上的,也就是说上面的写法等同于下面的写法:

class Person {};

Person.prototype = {
  constructor () {},
  sayHi () {},
};

    同样class声明可以采用表达式形式,如:

    const People = class Person {
      getName () {
        return Person.name;
      } 
    }

    需要注意的是,这个上面的类名是People而不是Person,Person只在内部使用指代当前的类;

    *与ES5中定义对象的不同之处:

    • class内部的所有方法都是不可枚举的,而ES5中的定义在原型上的方法是可枚举的

    • 不存在变量提升,也就是说在调用一个类之前必须先定义它,否则会报错

    • 不使用new操作符调用会报错,而ES中的构造函数不会

    • 类的内部默认使用的是严格模式