notes
  • Introduction
  • 前言
  • javascript
    • bind, call, apply 总结
    • Promise 总结
    • 事件循环(EventLoop)
    • 迭代器与生成器
    • 原型与原型链和继承
    • 函数防抖与函数节流
    • 性能优化需要知道的基础知识(HTTP)
    • 函数柯里化与反柯里化
  • react
    • Redux 小结
  • 其他
    • 资源推荐
    • DNS、Host、VPN 和科学上网
    • 脱坑手册
    • 安全
    • 正则表达式
    • 基于 rsync 同步文件
Powered by GitBook
On this page
  • 基本概念
  • 普通对象与函数对象
  • 构造函数
  • 原型对象
  • 原型链
  • 属性搜索原则
  • 所有函数对象的 __proto__ 都指向Function.prototype,它是一个空函数(Empty function)
  • 总结
  • JS继承
  • 寄生组合继承
  • 原型链继承
  • 借用构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • ES6中的继承
  • super 关键字
  • 类的原型( prototype )
  • Extends 的继承目标
  • 实例的 __proto__ 属性
  • 原生构造函数的继承

Was this helpful?

  1. javascript

原型与原型链和继承

JavaScript 中,万物皆对象,对象又分为:普通对象和函数对象,也就是Object 和 Function

基本概念

  • 对象:属性和方法的集合,即变量和函数的封装。每个对象都有一个__proto__属性,指向这个对象的构造函数的原型对象

  • 构造器函数:用于创建对象的函数,通过new关键字生成对象(构造函数的首字母必须大写,用来区分于普通函数)

  • 原型对象:每个函数都有一个prototype属性,它是一个指向原型对象的指针

  • 每个对象都有 __proto__ 属性,但只有函数对象才有 prototype 属性

普通对象与函数对象

举个栗子:

function f1(){};
var f2 = function();
var f3 = new Function('args','alert(args)');

// 函数对象 f1,f2,f3

var c1 = {};
var c2 = new Object();
var c3 = new function(){};

// 普通对象 c1,c2,c3

凡是通过new Function()创建的对象都是函数对象,其他的都是普通对象。Function Object 也都是通过 New Function()创建的所以它们也是函数对象

注意:尽管可以使用 Function 构造函数创建函数,但最好不要使用它,因为用它定义函数比用传统方式要慢得多。不过,所有函数都应看作 Function 类的实例。

构造函数

构造函数始终都应该以一个大写字母开头;构造函数本身也是函数,只不过可以用来创建对象;要创建构造函数的新实例,必须使用new操作符

举个栗子:

function Person(name){
    this.name = name;
    this.sayHello=function(){
        console.log(`hello ${this.name}`)
    }
}
var girl = new Person("lili");
console.log(girl.constructor===Person); //true
// constructor属性存在于原型对象中,他指向了构造函数
girl.sayHello(); //hello lili

构造函数调用所经历的步骤:

  • 创建一个新的对象

  • 将构造函数的作用域赋给新对象(因此this指向了这个新对象)

  • 执行构造函数中的代码(为这个新对象添加属性)

  • 返回新对象

构造函数存在的问题:每个方法都要在每个实例上重新创建一遍。(每定义一个函数就是实例化了一个对象),不同实例上的同名函数是不相等的。其解决方法便是使用原型模型

原型对象

每一个函数都有prototype属性,它指向一个对象,即原型对象。

function Person(name){
    this.name=name
}
var p1=new Person('lisi')
var p2=new Person('lili')

Person.prototype.constructor === Person

constructor指向构造函数,每个原型对象 ( Person.prototype ) 都有一个constructor属性,指向prototype属性所在的函数 ( Person )

原型链

JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做 __proto__ 的内置属性,用于指向创建它的函数对象的原型对象 prototype

function Person(name) {
    this.name = name
}
Person.prototype.say = () => {
    console.log('Hi !');
}
var p1 = new Person('lisi');
p1.say();

p1.__proto__ === Person.prototype

属性搜索原则

对象在访问属性的时候, 首先在当前对象中查找,找到就停止查找直接使用该属性,如果没有找到则顺着__proto__属性向上查找,直到查找到原型链的顶端 Object.prototype.__proto__

这种由 __proto__ 属性连接而成的链条就是 原型链

所有函数对象的 __proto__ 都指向Function.prototype,它是一个空函数(Empty function)

Number.__proto__ === Function.prototype  // true
Boolean.__proto__ === Function.prototype // true
String.__proto__ === Function.prototype  // true
Array.__proto__ === Function.prototype   // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身

Object.__proto__ === Function.prototype  // true
Function.__proto__ === Function.prototype // true

// 唯一一个typeof XXX.prototype为 function 的 prototype
console.log(typeof Function.prototype) // function
console.log(typeof Object.prototype)   // object
console.log(typeof Number.prototype)   // object
console.log(typeof Boolean.prototype)  // object
console.log(typeof String.prototype)   // object
console.log(typeof Array.prototype)    // object

console.log(Function.prototype) // 空函数(Empty function)

总结

Object.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true

Object 与 Function 都是函数对象,都是通过new Function()创建的,所以: Object.__proto__ 指向 Function.prototype, Function.__proto__ 指向 Function.prototype,

结论:所有函数对象的 __proto__ 都指向 Function.prototype

Function.prototype.__proto__ === Object.prototype  //true

Object.prototype.__proto__  //null

Function.prototype 这个函数对象比较特殊,它的 __proto__ 指向了 Object.prototype,而 Object.prototype 的 __proto__ 指向了 null

JS继承

寄生组合继承

思想:通过寄生方式,砍掉父类的实例属性,避免了组合继承的缺点(调用两次父类构造函数)

组合继承是继承的一种实现方式,下面会有介绍

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

function Bar(name, age) {
    Foo.call(this, name);
    this.age = age;
}
// 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
// 注意!现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
// Bar.prototype.constructor = Bar
Bar.prototype.sayAge = function () {
    console.log(this.age);
};
var a = new Bar("lili", 18);
a.sayName(); // lili
a.sayAge(); // 18

Object.create(Foo.prototype)里面的实现机制类似下面这样

function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

// ES6 之前 Bar.ptototype = Object.create( Foo.prototype );

// ES6新增了一个方法,Object.setPrototypeOf,可以直接创建关联,而且不用手动添加constructor属性 Object.setPrototypeOf( Bar.prototype, Foo.prototype ); console.log(Bar.prototype.constructor === Bar) // true

下面介绍一些其他方式实现继承,同时也是 寄生组合继承 的演进过程

原型链继承

思想:子类的原型指向父类的实例

function Foo() {
    this.name = 'lisi';
    this.arr = [1, 2];
    this.sayName = function () {
        console.log(this.name);
    }
}

function Bar() {

}
Bar.prototype = new Foo();

var s1 = new Bar();
var s2 = new Bar();
console.log(s1.arr); // [1, 2]
s1.arr.push(3);
console.log(s1.arr); // [1, 2, 3]
console.log(s2.arr); // [1, 2, 3]

s1.sayName(); // lisi
s1.name = 'zhangsan';
s1.sayName(); //zhangsan

缺点:

  • 无法向父类的构造函数传参

  • 来自原型对象的引用属性是所有实例共享的

借用构造函数继承

思想:借用父类的构造函数来增强子类实例,等于是把父类的实例属性复制一份给子类实例装上

function Foo(name) {
    this.name = name;
    this.arr = [1, 2];
    this.sayName = function () {
        console.log(this.name);
    }
}

function Bar(name) {
    Foo.call(this);
}

var s1 = new Bar();
var s2 = new Bar();
console.log(s1.arr); // [1, 2]
s1.arr.push(3);
console.log(s1.arr); // [1, 2, 3]
console.log(s2.arr); // [1, 2]

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法

  • 无法实现函数的复用,每个子类都有父类实例函数的副本

组合继承

思想:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Foo(name) {
    this.name = name;
    this.arr = [1, 2];
}

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

function Bar(name) {
    Foo.call(this, name); // 第二次调用
}

Bar.prototype = new Foo(); // 第一次调用
Bar.prototype.constructor = Bar;

var s1 = new Bar('zhangsan');
var s2 = new Bar('lisi');
console.log(s1.arr); // [1, 2]
s1.arr.push(3);
console.log(s1.arr); // [1, 2, 3]
console.log(s2.arr); // [1, 2]

s1.sayName(); // zhangsan
s2.sayName(); // lisi

缺点:

  • 调用了两次父类的构造函数

原型式继承

思想:借助原型,可以基于已有的对象创建新对象

function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

var obj = {
    name: 'lili'
};

var p = object(obj)
console.log(p.name)
//这样 p 对象就可以拿到 obj 的属性了

ES5新增了方法规范化了原型式继承。即: Object.create()

寄生式继承

思想:在继承对象的同时通过添加属性与方法增强这个新对象,原型式继承的加强版

function createAnother(o) {
    var obj = Object.create(o); // 通过调用函数创建一个新对象
    obj.sayHi = function () { // 给这个对象添加方法
        console.log("hi");
    }
    return obj; //返回这个新对象
}

function Foo() {}
Foo.prototype.sayName = function () {
    console.log(this.name);
}
var now = createAnother(Foo.prototype);
now.name = 'lili'
now.sayName(); // lili
now.sayHi(); // hi

ES6中的继承

ES6中可以声明类( class )提供了extends关键字实现类的继承,定义的类只是语法糖,目的是让我们用更简洁明了的语法创建对象及处理相关的继承。

class Foo {
    constructor(name) {
        this.name = name
    }
    sayName() {
        console.log(this.name)
    }
}
class Bar extends Foo {
    constructor(name, age) {
        super(name); // 调用父类的constructor(x)
        this.age = age
    }
    sayHi() {
        console.log('Hi my name is ');
        super.sayName();
    }
}

var s1 = new Bar('lili', 18);
s1.sayHi();

super 关键字

  • 作为函数调用时( 即super(...args)), super代表父类的构造函数。

  • 作为对象调用时( 即super.method() ), super代表父类。此时super即可以引用父类实例的属性和方法, 也可以引用父类的静态方法。

  • 子类必须在constructor方法中调用super方法,并且在使用this之前调用

ES5 的继承, 实质是先创造子类的实例对象this, 然后再将父类的方法添加到this上面( base.call(this)) ES6中的继承机制完全不同,实质是先创造父类的实例对象this( 所以必须先调用super方法,返回父类实例,如果不调用super方法, 子类就得不到this对象), 然后再用子类的构造函数修改this。

类的原型( prototype )

Bar.__proto__ === Foo // true
Bar.prototype.__proto__ === Foo.prototype //true

这样的结果是因为, 类的继承是按照下面的模式实现的。

class Foo {

}
class Bar {

}

// B 的实例继承 A 的实例
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(Bar, Foo);

Object.setPrototypeOf方法的实现:

Object.setPrototypeOf = function (obj, proto) {
    obj.__proto__ = proto;
    return obj;
}

Extends 的继承目标

extends关键字后面可以跟多种类型的值 class Bar extends F

F 只要是一个有prototype属性的函数, 就能被 Bar 继承。 由于函数都有prototype属性( 除了Function.prototype函数), 因此F可以是任意函数。

Function.prototype.prototype // undefined

分析:

class Foo {}
Foo.__proto__ === Function.prototype // true
Foo.prototype.__proto__ === Object.prototype // true

//ES5
var Foo = function () {

}
Foo.__proto__ === Function.prototype // true
Foo.prototype.__proto__ === Object.prototype // true

Foo就是一个普通函数,所以直接继承 Funciton.prototype 但是, Foo 调用后返回一个空对象( 即Object实例), 所以Foo.prototype.__proto__指向构造函数( Object) 的prototype属性。

class Bar extends null {}
Bar.__proto__ === Function.prototype // true
Bar.prototype.__proto__ === undefined // true

Bar是一个普通函数, 所以直接继承 Funciton.prototype 但是 Bar 调用后返回的对象不继承任何方法, 所以它的 __proto__ 指向Function.prototype, 即实质上执行了下面的代码:

class Bar extends null {
    constructor() {
        return Object.create(null);
    }
}

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(Bar)===Foo //true

实例的 __proto__ 属性

class Foo {
}
class Bar extends Foo {
}

var p1=new Foo();
var p2=new Bar();


console.log(p2.__proto__ === Bar.prototype); //true
console.log(Bar.prototype.__proto__===Foo.prototype); //true
console.log(p2.__proto__.__proto__===p1.__proto__); //true

原生构造函数的继承

原生构造函数是指语言内置的构造函数, 通常用来生成数据结构。 ECMAScript 的原生构造函数大致有下面这些

Boolean,Number,String,Array,Date,Function,RegExp,Error,Object
class MyArray extends Array {}
var arr = new MyArray();
arr[0] = 1;
console.log(arr); //  [1]

注意, 继承Object的子类, 有一个行为差异。

class NewObj extends Object {
    constructor() {
        super(arguments);
    }
}
var p=Object({ age: 18});
var p1 = new NewObj({
    age: 18
});
console.log(p); // Object {age: 18}
console.log(p1); // NewObj {}

上面代码中, NewObj继承了Object, 但是无法通过super方法向父类Object传参。 这是因为 ES6 改变了Object构造函数的行为, 一旦发现Object方法不是通过 new Object() 这种形式调用, ES6 规定Object构造函数会忽略参数。

Previous迭代器与生成器Next函数防抖与函数节流

Last updated 6 years ago

Was this helpful?