JavaScript总结【5】原型、继承和类

JavaScript原型、继承和类

原型

Prototype

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]],它要么为 null,要么就是对另一个对象的引用。当我们从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性

let animal = {
  eats: true,
  sleep: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// 现在这两个属性我们都能在 rabbit 中找到:
alert( rabbit.eats ); // true
alert( rabbit.sleep ); // true
alert( rabbit.jumps ); // true

原型链

就近继承

__proto__[[Prototype]] 的区别

  • __proto__[[Prototype]] 的 getter/setter
  • __proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型

for…in与继承

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// Object.keys 只返回自己的 key
alert(Object.keys(rabbit)); // jumps

// for..in 会遍历自己以及继承的键
for(let prop in rabbit) alert(prop); // jumps,然后是 eats

如果需要过滤掉原型里的属性,可以使用hasOwnProperty在条件语句体内排除

F.prototype

  1. 如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 [[Prototype]]
// 设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]] 赋值为 animal”
let animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal
alert( rabbit.eats ); // true
  1. 默认的 F.prototype,构造器属性

每个函数都有 "prototype" 属性,即使我们没有提供它。默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。上面的方法有一个企业宣布没把虎眼石让关于替换掉整个prototype,正确的做法是选择添加或删除prototype的属性,如Rabbit.prototype.eat,或者手动重建construor属性,如construor: Rabbit

原生的原型

  • Object.prototyped的原型是null
  • 其他对象的原型都继承自Object,形成原型链,提高了存储效率
  • 基本数据类型也有原型,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 StringNumberBoolean 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失
String.prototype.show = function() {
  alert(this);
};

"BOOM!".show(); // BOOM!

从原型中借用

借用是指我们从一个对象获取一个方法,并将其复制到另一个对象。

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join;

alert( obj.join(',') ); // Hello,world!

上面这段代码有效,是因为内建的方法 join 的内部算法只关心正确的索引和 length 属性。它不会检查这个对象是否是真正的数组。

原型方法,没有 proto 的对象

不推荐使用__proto__ ,推荐使用以下方法来代替:

  • Object.create(proto, [descriptors])—— 利用给定的 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象。
  • Object.getPrototypeOf(obj)—— 返回对象 obj[[Prototype]]
  • Object.setPrototypeOf(obj, proto)—— 将对象 obj[[Prototype]] 设置为 proto

基本语法

class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert(this.name);
  }

}
// 用法:
let user = new User("John");
user.sayHi();

class的本质

JavaScript中类的本质是函数

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// 佐证:User 是一个函数
alert(typeof User); // function

以上代码本质是

  1. 创建一个名为 User 的函数,该函数成为类声明的结果。该函数的代码来自于 constructor 方法(如果我们不编写这种方法,那么它就被假定为空)。
  2. 存储类中的方法,例如 User.prototype 中的 sayHi

与函数的区别:

  1. 通过 class 创建的函数具有特殊的内部属性标记 [[IsClassConstructor]]: true。因此,它与手动创建并不完全相同,必须使用new来调用它
  2. 类方法不可枚举。 类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false
  3. 类总是使用 use strict。 在类构造中的所有代码都将自动进入严格模式

类表达式

类可以在另外一个表达式中被定义,被传递,被返回,被赋值,如果类表达式有名字,那么该名字仅在类内部可见

// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass 这个名字仅在类内部可见
  }
};
new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容
alert(MyClass); // error,MyClass 在外部不可见

我们甚至可以动态的按需创建类:

function makeClass(phrase) {
  // 声明一个类并返回它
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

// 创建一个新的类
let User = makeClass("Hello");
new User().sayHi(); // Hello

计算属性名称

使用中括号 [...] 的计算方法名称示例:

class User {
  ['say' + 'Hi']() { // 计算属性方法
    alert("Hello");
  }

}

new User().sayHi();

类字段

  1. 类字段使用如下:
class User {
  name = "John";
}

let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined
  1. 使用类字段制作绑定方法:

    JavaScript 中的函数具有动态的 this。它取决于调用上下文。因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则 this 将不再是对其对象的引用。像下面这种情况就是出现了this丢失:

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined

解决方案:

  • 传递一个包装函数,例如 setTimeout(() => button.click(), 1000)
  • 将方法绑定到对象,例如在 constructor 中
  • 使用箭头函数**(推荐)**
class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello

// 类字段 click = () => {...} 是基于每一个对象被创建的,在这里对于每一个 Button 对象都有一个独立的方法,在内部都有一个指向此对象的 this。我们可以把 button.click 传递到任何地方,而且 this 的值总是正确的

类继承

语法Child extend Parent

  1. 重写方法

注意:箭头函数没有super

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed = speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }

  stop() {
    super.stop(); // 调用父类的 stop
    this.hide(); // 增加功能
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5
rabbit.stop(); // White Rabbit stands still. White rabbit hides!
  1. 重写constructor
  • 继承类的 constructor 必须调用 super(...),并且 (!) 一定要在使用 this 之前调用
  • 父类构造器总是会使用它自己字段的值,而不是被重写的那一个
  • 可以在一个 Child 方法中使用 super.method() 来调用 Parent 方法
  • 方法在内部的 [[HomeObject]] 属性中记住了它们的类/对象。这就是 super 如何解析父方法的。因此,将一个带有 super 的方法从一个对象复制到另一个对象是不安全的。

静态属性与静态方法

  1. 静态方法:

    可以把一个方法赋值给类的函数本身,而不是赋给它的 "prototype"。这样的方法被称为 静态方法.通常,静态方法用于实现属于该类但不属于该类任何特定对象的函数。静态方法也被用于与数据库相关的公共类,可以用于搜索/保存/删除数据库中的条目

class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true
  1. 静态属性
class Article {
  static publisher = "Levi Ding";
}

alert( Article.publisher ); // Levi Ding
// 等价于Article.publisher = "Levi Ding";
  1. 继承静态属性和方法

静态方法也可以继承

私有的和受保护的属性和方法

  1. 内部接口和外部接口

    • 内部接口是可以通过该类的其他方法访问,但不能从外部访问的方法和属性。
    • 外部接口是也可以从类的外部访问的方法和属性。
  2. 设置受保护的属性:

    • 受保护的属性通常以下划线 _ 作为前缀。
    • 只读:设置为访问器属性,且不设置setter

类检查

  1. instanceof操作符

instanceof 操作符用于检查一个对象是否属于某个特定的 class。同时,它还考虑了继承。obj instanceof Class如果 obj 隶属于 Class 类(或 Class 类的衍生类),则返回 true

  1. toString方法

自己创建的对象可以使用特殊的对象属性 Symbol.toStringTag 自定义对象的 toString 方法的行为。

let user = {
  [Symbol.toStringTag]: "User"
};

alert( {}.toString.call(user) ); // [object User]

Mixin

在 JavaScript 中构造一个 mixin 最简单的方式就是构造一个拥有实用方法的对象,以便我们可以轻松地将这些实用的方法合并到任何类的原型中。

// mixin
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};

// 用法:
class User {
  constructor(name) {
    this.name = name;
  }
}

// 拷贝方法
Object.assign(User.prototype, sayHiMixin);

// 现在 User 可以打招呼了
new User("Dude").sayHi(); // Hello Dude!

方式就是构造一个拥有实用方法的对象,以便我们可以轻松地将这些实用的方法合并到任何类的原型中。

// mixin
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  }
};

// 用法:
class User {
  constructor(name) {
    this.name = name;
  }
}

// 拷贝方法
Object.assign(User.prototype, sayHiMixin);

// 现在 User 可以打招呼了
new User("Dude").sayHi(); // Hello Dude!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值