理解对象

对象中有四个属性描述符分别是:

  1. [[Configurable]]:表示是否能通过delete删除该属性或者能否修改属性的特性。默认是true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const person = {
    name: 'barney',
    age: 25
    };

    Object.defineProperty(person, 'name', {
    value: 'barney',
    writable: true,
    configurable: false,
    enumerable: true
    });

    delete person.name;
    delete person.age;
    console.log(person.name, person.age); // barney undefined

    Object.defineProperty(person, 'name', {
    value: 'billyy',
    writable: true,
    configurable: true,
    enumerable: true
    }); // 这句话会报错,因为一旦[[configurable]]的值为false之后,就不能再转换为true了,是一个不可逆过程

    可以看到,当属性描述符[[configurable]]为false之后,无法通过delete删除该属性了。

  2. [[Enumberable]]:表示能否通过for...in循环返回属性。默认是true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function logProprties (obj) {
    for (const i in obj) {
    console.log(i);
    }
    }

    const person = {
    name: 'barney',
    age: 25
    };

    logProprties(person); // name age

    Object.defineProperty(person, 'name', {
    value: 'barney',
    writable: true,
    configurable: true,
    enumerable: false
    });

    logProprties(person); // age

    可以看到,当name的[[Enumberable]]为true的时候,通过for...in是可以访问到name属性的,但是改为false之后,就无法通过for...in访问到了。

  3. [[Writable]]:是否可以修改数据的值。默认是true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const person = {
    name: 'barney',
    age: 25
    };

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

    person.name = 'billy';
    console.log(person.name); // barney

    Object.defineProperty(person, 'name', {
    value: 'billy'
    });

    console.log(person.name); // billy

    可以看到,当[[Writable]]为false的时候,name的值已经不能直接通过赋值语句来修改了。只能通过Object.defineProperty来修改了。

  4. [[Value]]:当前属性的值,默认是undefiend

访问器属性:

  1. [[get]]:在读取的时候调用的函数,默认值是undefined
  2. [[set]]:在写入属性值 的时候调用的函数,默认值是undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const person = {
_name: 'barney',
age: 25
};

Object.defineProperty(person, 'name', {
get: function getName () {
return `my name is ${this._name}`;
},

set: function setName (val) {
this._name = val;
console.log('set name success');
}
});

console.log(person.name); // my name is barney
person.name = 'billy'; // set name success
console.log(person.name); // my name is billy

定义多个属性的特性可以用Object.defineProperties(),而获取一个对象的属性特性可以用Object.getOwnPropertyDescriptor()

创建自定义对象

一个对象其实就是数据和操作数据方法的集合。下面将介绍很多中创建对象的方法,是一个循序渐进的优化路径,最终只有一两个是常用的,但是这个优化思路需要逐步递进。

  1. 工厂模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function createPerson (name, age, job) {
    const obj = new Object();

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

    return obj;
    }

    const person = createPerson('barney', 25, 'engineer');

    工厂模式解决了创建多个相似对象的问题,但是每个对象与创建他们函数无法有太多联系,即无法知道personcreatePerson之间的关系。

  2. 构造函数模式:明确对象与构造函数之间的关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () { console.log(this.name); };
    }

    const person = new Person('barney', 25, 'engineer');
    person.sayName(); // barney
    console.log(person instanceof Person); // true

    可以通过instanceof来判断对象person是构造函数Person的实例。但是可以看到每个实例都自己实现了一次sayName方法,但是他们的行为是一样,所以我们应该只实现一次sayName方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function sayName () { console.log(this.name); }

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

    const person = new Person('barney', 25, 'engineer');
    person.sayName(); // barney

    可以看到,我们在Person外部定义一个sayName方法,这样就可以只实现一次sayName方法了。但是这样做的话,就将sayNamePerson解耦了。但其实sayNamePerson特有的方法,其他的对象是不能调用的。

  3. 构造函数+原型链模式:解决共享函数的问题。(重要)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 构造函数
    function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    }

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

    const person = new Person('barney', 25, 'engineer');
    person.sayName(); // barney
    console.log(person instanceof Person); // true

    可以看到,数据等每个实例特有的属性,都放在构造函数Person中,而对这些数据的共同操作则放在Person.prototype上,这样相同的行为逻辑就只实现了一次,而且还是在Person的原型链上。这样,其他的类型的对象就没有办法直接调用sayName这个方法了。而这种模式也是比较常用的模式。

继承

继承是面向对象的一个非常重要且常用的手段,JS中的继承方法主要有两个。

  1. 组合继承

    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
    function User (name) {
    this.name = name;
    }

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

    function Vip (name, level) {
    User.call(this, name);

    this.level = level;
    }

    // 将prototype指向User的实例,User的实例的[[prototype]]指向User.prototype,这样Vip.prototype就可以访问User.prototype上的方法了。
    Vip.prototype = new User();
    // 上一句修改了Vip.prototype.constructor的指向,指向了Object.prototype,所以现在要重新指向默认的Vip
    Vip.prototype.construtor = Vip;

    // 定义新的方法只能通过Vip.prototype.xxx的格式,不能用字面量表示法
    Vip.prototype.sayLevel = function () { console.log(this.level); };

    const user = new User('Billy');
    user.sayName();
    console.log(user instanceof User);

    const vip = new Vip('Barney', 2);
    vip.sayName();
    vip.sayLevel();
    console.log(vip instanceof Vip);
    console.log(vip instanceof User);
  2. 组合继承中可以看到,User构造函数被调用了两次,其中一次是为了让Vip.prototypeUser.prototype之间产生链接,我们将这一步优化一下,创建一个指向User.prototype的对象就可以了。这样User就只需要调用一次,而且可读性也更强。

    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
    function User (name) {
    this.name = name;
    }

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

    function Vip (name, level) {
    User.call(this, name);

    this.level = level;
    }

    const proto = User.prototype;
    proto.constructor = Vip;
    Vip.prototype = proto;

    Vip.prototype.sayLevel = function () { console.log(this.level); };

    const user = new User('Billy');
    user.sayName(); // Billy
    console.log(user instanceof User); // true

    const vip = new Vip('Barney', 2);
    vip.sayName(); // Barney
    vip.sayLevel(); // 2
    console.log(vip instanceof Vip); // true
    console.log(vip instanceof User); // true

Class

JS中的Class是更接近面向对象的表达方式,但是Class的实质仍然是Function,所以,可以把Class看作是Function的一种语法糖。具体类如何实现上面的UserVip,看示例

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
class User {
constructor (name) {
this.name = name;
}

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

class Vip extends User {
constructor (name, level) {
super(name);

this.level = 2;
}

sayLevel () {
console.log(this.level);
}
}

const vip = new Vip('Barney', 2);
vip.sayName();
vip.sayLevel();

这样写,可读性更好。需要注意的是,子类中的constructor调用super方法。而关于更多class的细节问题参考: https://es6.ruanyifeng.com/#docs/class和https://es6.ruanyifeng.com/#docs/class-extends