ES5 继承
ES5中没有类的概念,通常通过声明一个构造函数来模拟类
function Person(name) {
this.name = name;
this.sayName1 = function () {
console.log(this.name + '在工作');
}
}
// 原型链上的方法和属性会被多个实例共享,构造函数中的则不会
Person.prototype.sayName2 = function () {
console.log(this.name + '在学习');
}
// 静态方法
Person.sayName3 = () => {
console.log(this.name + '在运动')
}
var person = new Person('Tom');
person.sayName1(); // Tom在工作
person.sayName2(); // Tom在学习
原型方法和实例方法的区别:
- 写在原型中的方法可以被所有的实例共享, 实例化的时候不会在实例内存中再复制一份,占有的内存消耗少。
- js中每个函数都有一个prototype属性,这个属性指向一个对象(所有属性的集合:默认constructor属性,值指向这个函数本身。)
- 每个原型对象都属于对象,所以它也有自己的原型,而它自己的原型对象又有自己的原型,所以就形成了原型链。
- 一个对象的隐式原型指向构造这个对象的构造函数的显式原型,所以这个对象可以访问构造函数的属性和方法。(new一个实例)
- js的继承也就是通过原型链来实现的,当访问一个对象的属性,如果这个对象本身不存在,则沿着proto依次往上查找,如果有则返回值,没有则一直到查到Object.prototype的proto的值为null.
继承
ES5实现继承的方式有原型链继承,构造继承,实例继承,拷贝继承,组合继承,寄生组合继承这六种
原型链继承
- 优点
-
- 实例是子类的实例,也是父类的实例
- 可以调用父类的实例属性和方法,也可以调用父类原型链上的属性和方法
- 缺点
-
- 子类无法在构造器中新增属性或者方法,必须要在
new Person()之后 - 在子类实例化时,无法向父类传参
- 父类原型对象的所有属性被所有实例共享
- 子类无法在构造器中新增属性或者方法,必须要在
// 原型链继承
function Cat() { }
Cat.prototype = new Person('Pt'); // 只能在这里向父类传参,或者下面代码那样
var cat = new Cat();
cat.sayName1();
构造继承
- 优点
-
- 解决了
原型链继承中,子类实例共享父类引用属性的问题 - 在创建子类实例时,可以向父类传参
- 解决了
- 缺点
-
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类构造函数中的属性和方法,不能继承父类原型链中的属性和方法
- 无法实现函数的复用,每个子类都有父类实例函数的副本,影响性能
function Cat(name) {
Person.call(this, name);
}
var cat = new Cat('Tom');
cat.sayName1();
实例继承
- 缺点
-
- 无法实现多继承
- 子类实例化出来的对象是父类类型,不是子类类型
// 实例继承
function Cat(name) {
var instance = new Person(name);
return instance;
}
var cat = new Cat('Tom');
cat.sayName1()
组合继承
- 优点
-
- 弥补了
构造继承的缺陷,可以继承实例的属性/方法,也可以继承原型上的属性/方法 - 既是子类的实例,也是父类的实例
- 可以向父类传参
- 函数可以复用
- 弥补了
- 缺点
-
- 调用了两次父类的构造函数,生成了两份实例
function Cat(name, age) {
Person.call(this, name);
this.age = age;
}
Cat.prototype = new Person();
var cat = new Cat('Tom', 18);
console.log(cat.name);
cat.sayName1();
cat.sayName2();
console.log(cat.age);
寄生组合继承
function Cat(name) {
Person.call(this, name);
}
var Temp = Object.create(Person.prototype); // 创建对象,创建父类原型的一个副本
Temp.constructor = Cat; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
Cat.prototype = Temp; // 指定对象,将新创建的对象赋值给子类的原型
var cat = new Cat('Tom');
cat.sayName1()
new的时候都做了什么?
- 创建一个新对象
- 把这个新对象的
__proto__属性指向你要new 的那个对象的prototype - 让构造函数里面的
this指向新的对象,然后执行构造函数 - 返回这个新对象
function _new(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.call(obj, ...args);
return result instanceof Object ? result : obj;
}
ES6 Class
ES6提供了更接近传统的写法,基本上可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到
class Person {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name + '在学习')
}
}
const p = new Person('jiacheng')
构造函数上的prototype属性,在ES6类上面继续存在,事实上,类的所有方法都是定义在类的prototype属性上面的
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
因此,在类的实例上面调用方法,其实就是调用原型上的方法
class B{}
const b = new B();
b.constructor === B.prototype.constructor // true
所以,使用Object.assin()方法可以很方便地一次向类添加多个方法
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
prototype对象的constructor属性,直接指向类本身,这与ES5的行为是一致的
Point.prototype.constructor === Point // true
另外,类内部所有定义的方法,都是不可枚举的
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
constructor
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法,一个类即使没有显示定义constructor,也会被默认添加一个空的constructor
class Person{}
// 相当于
class Person {
constructor(){}
}
constructor默认返回实例对象,完全可以指定返回另一个对象
class Person {
constructor() {
return Object.create(null)
}
}
new Person instanceof Person // false
类必须使用new调用,否则会直接报错
class Person {
}
Person()
// TypeError: Class constructor Foo cannot be invoked without 'new'
类的实例
直接通过new命令生成一个类的实例,与ES5不同的是,直接调用类会报错
class Point {
// ...
}
// 报错
var point = Point(2, 3);
// 正确
var point = new Point(2, 3);
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
类的所有实例共享一个原型对象
var p1 = new Point(2,3);
var p2 = new Point(3,2);
//true
p1.__proto__ === p2.__proto__
这也意味着,可以通过实例的__proto__属性为“类”添加方法。
__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
上面代码在p1的原型上添加了一个printName()方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。
getter和setter
与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
存值函数和取值函数是设置在属性的 Descriptor 对象上的。
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.prototype, "html"
);
"get" in descriptor // true
"set" in descriptor // true
注意点
- 严格模式
类和模块的内部,默认就是严格模式,不需要使用'use strict'指定运行模式 - 不存在提升
类不存在变量提升 - Generator方法
如果某个方法前加上*,就表示该方法是一个Generator函数
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。
- this的指向
类方法内部如果含有this,它默认指向类的实例,但该方法无法单独使用
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
上面代码中,因为printName方法中的this,默认指向Logger类的实例,但如果将这个方法单独提取出来使用,this会指向该方法运行时所在的环境,而由于class内部是严格模式,所以这时候this直接指向undefined,导致报错
解决方法:
-
- 在构造方法中绑定
this,这样就不会找不到print方法了。
- 在构造方法中绑定
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
-
- 使用箭头函数
箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象
- 使用箭头函数
class Person {
printName = () => {
this.print()
}
print() {
console.log('111');
}
}
var p = new Person();
var { printName } = p;
printName()
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承,如果在一个方法前面加上static关键字,就表示该方法不会被实例继承,而是通过类来调用,这被称为静态方法
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
- 如果静态方法中包含this关键字,这个this指向的是类,而不是实例
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
- 父类的静态方法,可以被子类继承
- 静态方法也可以从
super对象上调用
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
静态属性
静态属性是指class本身的属性,而不是定义在实例对象上的属性
class Foo {
}
Foo.prop = 1;
// 或者
class Foo{
static name = 'jiacheng'
}
Foo.name; // jiacheng
继承
class通过extends关键字来实现继承
class Person{}
class ColorPoint extends Person {}
- super
super关键字表示父类的构造函数,用来新建父类的this对象
子类必须在constructor中调用super方法,否则会报错
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
ES6继承和ES5继承的区别
- ES5里的构造函数是一个普通函数,可以使用new调用,也可以直接调用,且存在变量提升。ES6的class必须使用new操作符调用,且不存在变量提升
- ES5子类的原型是指向
Function.prototype,而ES6子类的原型是指向父类的 - ES5的原型方法和静态方法是可枚举的,而class的默认不可枚举,但可以使用
Object.getOwnPropertyNames方法获取 - ES5的继承,实质是先创造一个子类实例对象的
this,然后再执行父类构造函数给它添加实例方法和属性(不执行也无所谓);ES6的继承机制则相反,先将父类的属性和方法,加到一个空对象上,然后再将该对象作为子类的实例,即"继承在前,实例在后"