1. 对象基础

7. 对象
之前写过对象基本了解,这里就不再继续写了

1.1 获取对象名

  1. let object={
  2. name:name,
  3. age:age,
  4. sex:sex,
  5. number:number
  6. };
  7. let arr=Object.keys(object)
  8. console.log(arr) //[name,age,sex,number]

2.继承

  • 继承是面向对象编程中讨论最多的话题。
  • 很多面向对象语言都支持两种继承:
    • 接口继承
    • 实现继承。
  • 前者只继承方法签名,后者继承实际的方法。
  • 接口继承在 ECMAScript 中是不可能的,因为函数没有签名。
  • 实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。


    2.1 原型链

    ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。

  • 其基本思想就是通过原型继承多个引用 类型的属性和方法。

  • 重温一下构造函数、原型和实例的关系:
    1. - 每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
    2. - 如果原型是另一个类型的实例呢?那就意味 着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。
    3. - 这样就在实例和原型之间构造了一条原型链。
    4. - 这就是原型链的基本构想。

简单来说,对象实例都是由构造函数内部的对象原型来创造的,对象原型相当于一类实例对象的模板,由构造函数包含,但原型对象也可能是其他构造函数内原型对象的实例对象,这样就构成了两个构造函数之间的关系

在这里仅仅浅析一下什么是原型链,关于原型链的问题还是在有所了解后再深入

2.2 call()

  • call()可以调用函数
  • call()可以修改this的指向,使用call()的时候 参数一是修改后的this指向,参数2,参数3..使用逗号隔开连接
    1. function fn(x, y) {
    2. console.log(this);
    3. console.log(x + y);
    4. }
    5. var o = {
    6. name: 'andy'
    7. };
    8. fn.call(o, 1, 2);//调用了函数此时的this指向了对象o,

2.3 子构造函数继承父构造函数中的属性

  1. 先定义一个父构造函数
  2. 再定义一个子构造函数
  3. 子构造函数继承父构造函数的属性(使用call方法)
    1. // 1. 父构造函数
    2. function Father(uname, age) {
    3. // this 指向父构造函数的对象实例
    4. this.uname = uname;
    5. this.age = age;
    6. }
    7. // 2 .子构造函数
    8. function Son(uname, age, score) {
    9. // this 指向子构造函数的对象实例
    10. 3.使用call方式实现子继承父的属性
    11. Father.call(this, uname, age);
    12. this.score = score;
    13. }
    14. var son = new Son('someOne', 18, 100);
    15. console.log(son);

2.4 借用原型对象继承方法

  1. 先定义一个父构造函数
  2. 再定义一个子构造函数
  3. 子构造函数继承父构造函数的属性(使用call方法)

    1. // 1. 父构造函数
    2. function Father(uname, age) {
    3. // this 指向父构造函数的对象实例
    4. this.uname = uname;
    5. this.age = age;
    6. }
    7. Father.prototype.money = function() { //原型对象添加方法
    8. console.log(100000);
    9. };
    10. // 2 .子构造函数
    11. function Son(uname, age, score) {
    12. // this 指向子构造函数的对象实例
    13. Father.call(this, uname, age);
    14. this.score = score;
    15. }
    16. // Son.prototype = Father.prototype; 这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化
    17. Son.prototype = new Father();
    18. // 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
    19. Son.prototype.constructor = Son;
    20. // 这个是子构造函数专门的方法
    21. Son.prototype.exam = function() {
    22. console.log('考试');
    23. }
    24. var son = new Son('someOne', 18, 100);
    25. console.log(son)

3. 类

ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。类(class)是 ECMAScript 中新的基础性语法糖结构,因此刚开始接触时可能会不太习惯。虽然 ECMAScript 6 类表面 上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。

3.1 定义类

与函数类型相似,定义类也有两种主要方式:类声明和类表达式。这两种方式都使用 class 关键字加大括号:

  1. // 类声明
  2. class Person {}
  3. // 类表达式
  4. const Animal = class {};
  • 与函数表达式类似,类表达式在它们被求值前也不能引用。
  • 不过,与函数定义不同的是,虽然函数声明可以提升,但类定义不能 ```javascript console.log(fn); // undefined var fn = function() {}; //fn为变量,只提升变量不提升函数 console.log(fn); // function() {}

console.log(fn1); // fn1() {} function fn1() {} //函数声明提升 console.log(fn1); // fn1() {}

console.log(aClass); // undefined var aClass = class {}; //类声明不进行提升 ,但声明的变量名提升 console.log(aClass); // class {}

console.log(otherClass); // ReferenceError: otherClass is not defined class otherClass {} // 类表达式声明都不会提升 console.log(otherClass); // class otherClass {}

  1. 另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作用域限制:
  2. ```javascript
  3. {
  4. function newFun() {}
  5. class oClass {}
  6. }
  7. console.log(newFun); // newFun() {}
  8. console.log(oClass); // ReferenceError: oClass is not defined

和let一样的限制都在块{}括号内部存在


3.2 类的构成

  • 类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。 空的类定义照样有效。
  • 默认情况下,类定义中的代码都在严格模式下执行。
  • 与函数构造函数一样,多数编程风格都建议类名的首字母要大写,以区别于通过它创建的实例(比 如,通过 class Foo {}创建实例 foo): ```javascript // 类可以定义为空 class Foo {}

// 有构造函数的类,有效 class Bar { constructor() {} }

// 有获取函数的类,有效 class Baz { get myBaz() {} }

// 有静态方法的类,有效 class Qux { static myQux() {} }

  1. 类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称 字符串。但不能在类表达式作用域外部访问这个标识符。
  2. ```javascript
  3. let Person = class PersonName { //类声明创建类,并将类赋值给一个变量
  4. identify() { //里面包含一个函数
  5. console.log(Person.name, PersonName.name);
  6. }
  7. }
  8. let p = new Person(); //通过new调用类里面的构造函数
  9. p.identify(); // PersonName PersonName 类赋值给p 含有此函数可在外部运行
  10. console.log(Person.name); // PersonName
  11. console.log(PersonName); // ReferenceError: PersonName is not defined

外部调用类名无法调用需要将类赋值给变量,通过变量.name的方式来获取类名


3.3 类构造函数

  • constructor 关键字用于在类定义块内部创建类的构造函数。
  • 方法名 constructor 会告诉解释器 在使用 new 操作符创建类的新实例时,应该调用这个函数。
  • 构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。

    实例化


    使用 new 操作符实例化上面代码中 Person 的操作等于使用 new 调用其构造函数。唯一可感知的不同之处就是,JavaScript 解释器知道使用 new 和类意味着应该使用 constructor 函数进行实例化。使用 new 调用类的构造函数会执行如下操作。
    (1) 在内存中创建一个新对象。
    (2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
    (3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
    (4) 执行构造函数内部的代码(给新对象添加属性)。
    (5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
  1. class Animal {} //创键一个类
  2. class Person { //创建第二个类
  3. constructor() {
  4. console.log('person ctor');
  5. }
  6. }
  7. class Vegetable { //创建第三个类
  8. constructor() {
  9. this.color = 'orange';
  10. }
  11. } //使用new操作符实例化类
  12. let a = new Animal();
  13. let p = new Person(); // person ctor
  14. let v = new Vegetable();
  15. console.log(v.color); // orange 类实例化后可在外部调用类的属性

类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:

  1. class Person {
  2. constructor(name) {
  3. console.log(arguments.length);
  4. this.name = name || null; //或判断符,只有两个都为false才结束,输出第二个表达式null
  5. }
  6. }
  7. let p1 = new Person; // 0 0=false
  8. console.log(p1.name); // null
  9. let p2 = new Person(); // 0
  10. console.log(p2.name); // null
  11. let p3 = new Person('Jake'); // 1=teue 或运算符不再判断第二个表达式直接赋值给name
  12. console.log(p3.name); // Jake

类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果 不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。调用类构造函数时如果 忘了使用 new 则会抛出错误:

  1. function Person() {}
  2. class Animal {}
  3. // 把 window 作为 this 来构建实例
  4. let p = Person();
  5. let a = Animal();
  6. // TypeError: class constructor Animal cannot be invoked without 'new'

类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例方法(但作为类构造函数,仍然 要使用 new 调用)。因此,实例化之后可以在实例上引用它:

  1. class Person {}
  2. // 使用类创建一个新实例
  3. let p1 = new Person();
  4. p1.constructor();
  5. // TypeError: Class constructor Person cannot be invoked without 'new'
  6. // 使用对类构造函数的引用创建一个新实例
  7. let p2 = new p1.constructor();

把类当成特殊函数

ECMAScript 中没有正式的类这个类型。从各方面来看,ECMAScript 类就是一种特殊函数。声明一 个类之后,通过 typeof 操作符检测类标识符,表明它是一个函数:

  1. class Person {}
  2. console.log(Person); // class Person {}
  3. console.log(typeof Person); // function

类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类自身:

  1. class Person{}
  2. console.log(Person.prototype); // { constructor: f() }
  3. console.log(Person === Person.prototype.constructor); // true

与普通构造函数一样,可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中:

  1. class Person {}
  2. let p = new Person();
  3. console.log(p instanceof Person); // true

类是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递:

  1. // 类可以像函数一样在任何地方定义,比如在数组中
  2. let classList = [
  3. class {
  4. constructor(id) {
  5. this.id_ = id;
  6. console.log(`instance ${this.id_}`);
  7. }
  8. }
  9. ];
  10. function createInstance(classDefinition, id) {
  11. return new classDefinition(id);
  12. }
  13. let foo = createInstance(classList[0], 3141); // instance 3141

与立即调用函数表达式相似,类也可以立即实例化:

  1. // 因为是一个类表达式,所以类名是可选的
  2. let p = new class Foo {
  3. constructor(x) {
  4. console.log(x);
  5. }
  6. }('bar'); // bar
  7. console.log(p); // Foo {}

3.4 类 继承

ECMAScript 6 新增特性中最出色的一 个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链。

继承基础

ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。 很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容):

  1. class Vehicle {}
  2. // 继承类
  3. class Bus extends Vehicle {}
  4. let b = new Bus(); //类的实例化
  5. console.log(b instanceof Bus); // true
  6. console.log(b instanceof Vehicle); // true
  7. function Person() {}
  8. // 继承普通构造函数
  9. class Engineer extends Person {}
  10. let e = new Engineer();
  11. console.log(e instanceof Engineer); // true
  12. console.log(e instanceof Person); // true

注意 extends 关键字也可以在类表达式中使用,因此let Bar = class extends Foo {}是有效的语法。

构造函数、HomeObject 和 super()

派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅 限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。

  1. class Vehicle {
  2. constructor() {
  3. this.hasEngine = true;
  4. }
  5. }
  6. class Bus extends Vehicle {
  7. constructor() {
  8. // 不要在调用 super()之前引用 this,否则会抛出 ReferenceError
  9. super(); // 相当于 super.constructor()
  10. console.log(this instanceof Vehicle); // true
  11. console.log(this); // Bus { hasEngine: true }
  12. }
  13. }
  14. new Bus(); //类实例化

在静态方法中可以通过 super 调用继承的类上定义的静态方法:

  1. class Vehicle {
  2. static identify() {
  3. console.log('vehicle');
  4. }
  5. }
  6. class Bus extends Vehicle {
  7. static identify() {
  8. super.identify();
  9. }
  10. }
  11. Bus.identify(); // vehicle

注意 ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个 指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部 访问。super 始终会定义为[[HomeObject]]的原型。

在使用 super 时要注意几个问题:

  • super 只能在派生类构造函数和静态方法中使用

    1. class Vehicle {
    2. constructor() {
    3. super();
    4. // SyntaxError: 'super' keyword unexpected
    5. }
    6. }
  • 不能单独使用super关键字,要么用它调用构造函数,要么用它引用静态方法

    1. class Vehicle {}
    2. class Bus extends Vehicle {
    3. constructor() {
    4. console.log(super);
    5. // SyntaxError: 'super' keyword unexpected here
    6. }
    7. }
  • 调用 super()会调用父类构造函数,并将返回的实例赋值给 this。

    1. class Vehicle {}
    2. class Bus extends Vehicle {
    3. constructor() {
    4. super();
    5. console.log(this instanceof Vehicle);
    6. }
    7. }
    8. new Bus(); // true
  • super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。

    1. class Vehicle {
    2. constructor(licensePlate) {
    3. this.licensePlate = licensePlate;
    4. }
    5. }
    6. class Bus extends Vehicle {
    7. constructor(licensePlate) {
    8. super(licensePlate);
    9. }
    10. }
    11. console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
  • 如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的参数

    1. class Vehicle {
    2. constructor(licensePlate) {
    3. this.licensePlate = licensePlate;
    4. }
    5. }
    6. class Bus extends Vehicle {}
    7. console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }

    在类构造函数中,不能在调用 super()之前引用 this。

    1. class Vehicle {}
    2. class Bus extends Vehicle {
    3. constructor() {
    4. console.log(this);
    5. }
    6. }
    7. new Bus();
    8. // ReferenceError: Must call super constructor in derived class
    9. // before accessing 'this' or returning from derived constructor
  • 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象。

    1. class Vehicle {}
    2. class Car extends Vehicle {}
    3. class Bus extends Vehicle {
    4. constructor() {
    5. super();
    6. }
    7. }
    8. class Van extends Vehicle {
    9. constructor() {
    10. return {};
    11. }
    12. }
    13. console.log(new Car()); // Car {}
    14. console.log(new Bus()); // Bus {}
    15. console.log(new Van()); // {}

继承内置类型

ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:

  1. class SuperArray extends Array {
  2. shuffle() {
  3. // 洗牌算法
  4. for (let i = this.length - 1; i > 0; i--) {
  5. const j = Math.floor(Math.random() * (i + 1));
  6. [this[i], this[j]] = [this[j], this[i]];
  7. }
  8. }
  9. }
  10. let a = new SuperArray(1, 2, 3, 4, 5);
  11. console.log(a instanceof Array); // true
  12. console.log(a instanceof SuperArray); // true
  13. console.log(a); // [1, 2, 3, 4, 5]
  14. a.shuffle();
  15. console.log(a); // [3, 1, 4, 5, 2]

有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的:

  1. class SuperArray extends Array {}
  2. let a1 = new SuperArray(1, 2, 3, 4, 5);
  3. let a2 = a1.filter(x => !!(x%2))
  4. console.log(a1); // [1, 2, 3, 4, 5]
  5. console.log(a2); // [1, 3, 5]
  6. console.log(a1 instanceof SuperArray); // true
  7. console.log(a2 instanceof SuperArray); // true

如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的 实例时使用的类:

  1. class SuperArray extends Array {
  2. static get [Symbol.species]() {
  3. return Array;
  4. }
  5. }
  6. let a1 = new SuperArray(1, 2, 3, 4, 5);
  7. let a2 = a1.filter(x => !!(x%2))
  8. console.log(a1); // [1, 2, 3, 4, 5]
  9. console.log(a2); // [1, 3, 5]
  10. console.log(a1 instanceof SuperArray); // true
  11. console.log(a2 instanceof SuperArray); // false

3.5 实例、原型和类成员

类的语法可以非常方便地定义应该存在于实例上的成员、原型上的成员和类本身上的成员

实例成员

通过new调用类标识符(即类名),都会执行类构造函数。这个函数的内部可以为新创建的实例(this)添加“自有”属性。至于添加什么属性是没有限制的,在构造函数执行完毕后,依然可以给已经创建的实例添加新成员(新属性新方法)
每一个实例都对应一个唯一的成员对象,这意味着所有成员不会再原型上共享

  1. class Person{
  2. constructor(){
  3. // 这个例子先使用对象包装类型定义一个字符串
  4. // 为的是在下面测试两个对象的相等性
  5. this.name = new String('Jack');
  6. this.sayName = () => console.log(this.name);
  7. this.nicknames = ['Jake', 'J-Dog']
  8. }
  9. }
  10. let p1 = new Person(),
  11. p2 = new Person();
  12. p1.sayName(); // Jack
  13. p2.sayName(); // Jack
  14. console.log(p1.name === p2.name); // false
  15. console.log(p1.sayName === p2.sayName); // false
  16. console.log(p1.nicknames === p2.nicknames); // false
  17. p1.name = p1.nicknames[0];
  18. p2.name = p2.nicknames[1];
  19. p1.sayName(); // Jake
  20. p2.sayName(); // J-Dog

原型方法与访问器

为了在类的实例中共享方法,类定义语法把类块中定义的方法作为原型方法

  1. class Person {
  2. constructor() {
  3. // 添加到 this 的所有内容都会存在于不同的实例上
  4. this.locate = () => console.log('instance');
  5. }
  6. // 在类块中定义的所有内容都会定义在类的原型上
  7. locate() {
  8. console.log('prototype');
  9. }
  10. }
  11. let p = new Person();
  12. p.locate(); // instance
  13. Person.prototype.locate(); // prototype 说明类中直接定义的方法都为原型对象方法

可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据

  1. class Person {
  2. name: 'Jake' //属性写在类构造函数constructot里
  3. }
  4. // Uncaught SyntaxError: Unexpected token 报错啦

类方法等同于对象属性,因此可以使用字符串、符号、计算的值作为键

  1. const symbolKey = Symbol('symbolKey');
  2. class Person {
  3. stringKey() {
  4. console.log('invoked stringKey');
  5. }
  6. [symbolKey]() {
  7. console.log('invoked symbolKey');
  8. }
  9. ['computed' + 'Key']() {
  10. console.log('invoked computedKey');
  11. }
  12. }
  13. let p = new Person();
  14. p.stringKey(); // invoked stringKey
  15. p[symbolKey](); // invoked symbolKey
  16. p.computedKey(); // invoked computedKey

类定义也支持获取和设置访问器,语法与行为跟普通对象一样:

  1. set name(newName) {
  2. this.name_ = newName;
  3. }
  4. get name() {
  5. return this.name_;
  6. }
  7. }
  8. let p = new Person();
  9. p.name = 'Jake';
  10. console.log(p.name); // Jake


静态方法

可以在类上定义静态方法,因为写在类里面的方法都为原型对象内部实例共享的方法,所以静态方法需要在其前面添加 static关键字作为前缀,在静态成员中,this引用类自身,其他所有都和原型成员一样

  1. class Person {
  2. constructor() {
  3. // 添加到 this 的所有内容都会存在于不同的实例上
  4. this.locate = () => console.log('instance', this);
  5. }
  6. // 定义在类的原型对象上
  7. locate() {
  8. console.log('prototype', this);
  9. }
  10. // 定义在类本身上
  11. static locate() {
  12. console.log('class', this);
  13. }
  14. }
  15. let p = new Person();
  16. p.locate(); // instance, Person {}
  17. Person.prototype.locate(); // prototype, {constructor: ... }
  18. Person.locate(); // class, class Person {}

非函数原型和类成员

虽然类定义并不是显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加

  1. class Person {
  2. sayName() {
  3. console.log(`${Person.greeting} ${this.name}`);
  4. }
  5. }
  6. // 在类上定义数据成员
  7. Person.greeting = 'My name is';
  8. // 在原型上定义数据成员
  9. Person.prototype.name = 'Jake';
  10. let p = new Person();
  11. p.sayName(); // My name is Jake