前言

在JavaScript中对象是一个复杂又简单的概念,而原型链和对象的关系又是紧密的联系的。但是原型链本身又是复杂的,也是JavaScript中继承的基本条件

一、构造函数

构造函数有点像其他语言的类,可以使用new运算符可以创建一个实例,构造函数分两类:

  • 内置
    • ObjectArrayFunctionDateMapSet等等
  • 自定义
    • new运算符调用的并且使用function关键字定义的函数(箭头函数不能被作为构造函数)

二、对象

在这里先看比较普通或者比较容易理解的对象:{},这种直接大括号定义的方式叫做字面量方式。还有一种是构造函数方式

// 字面量方式
let obj = {};
console.log(Object.prototype.toString.call(obj)); // "[object Object]"
// 构造函数方式
let obj2 = new Object();
console.log(Object.prototype.toString.call(obj2)); // "[object Object]"
// 自定义构造函数方式
function P() {
}
let obj3 = new P();
console.log(Object.prototype.toString.call(obj3)); // "[object Object]"

Object.prototype.toString可以准确的获取一个变量的数据类型,可以看到上面这三个对象的数据类型均为object

除了类型为object的才叫对象之外,还有其他数据类型也可以称为对象,这是js比较有意思的地方(下面会介绍为何万物皆对象)。例如:数组、函数、Date等等这些都称为较复杂对象

数组

let arr = [];
console.log(Object.prototype.toString.call(arr)); // "[object Array]"

// 构造函数方式
let arr2 = new Array();
console.log(Object.prototype.toString.call(arr2)); // "[object Array]"

函数

// 字面量方式
function fn() {
  return 'fn';
}
console.log(Object.prototype.toString.call(fn)); // "[object Function]"

// 构造函数方式
let fn2 = new Function('fn2','return \'fn2\'');
console.log(Object.prototype.toString.call(fn2)); // "[object Function]"

其他…

三、原型链

在介绍原型之前先思考以下问题:

  • 数组为什么可以调用push方法
  • 字符串为什么可以调用slice方法
let str = 'name';
console.log(str.slice(0,2)); // na

let arr = [];
arr.push(1);
console.log(arr); // [1]

这都归功于原型,在每个对象身上都有一个属性叫__proto__,该属性是一个对象。指向是该对象的构造函数的prototype,而这个prototype又是一个对象,一般情况下这个prototype是作为构造函数的属性呈现。简单来讲就是__proto__是对象(或者实例)下的属性,prototype是构造函数的属性

可以看出,上面提出的这个概念比较绕,先用一断代码演示比较常用的对象来解释上面的概念

let obj = {};
console.log(obj.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, …}
console.log(obj.__proto__ === Object.prototype); // true

可以看出一个对象上面的__proto__属性的确是一个对象,那么指向的也是该构造函数prototype

原型链的基础是在于‘原型’,一个‘原型’又有‘原型’,以此类推所以形成原型链。在访问一个对象的属性或者方法时首先会在自己的身上查找是否存在,否则就会顺着链上找,看个例子

对象的原型链

let obj = {
  name: 'my name'
};
console.log(obj.hasOwnProperty('name')); // true

在obj这个对象并没有定义hasOwnProperty这个方法,但是为什么可以去调用呢?原来、根据原型链的定义,首先它会在自己的属性上查找是否存在这个方法,否则会在proto原型上去查找。现在就来找找这个原型链上方法定义在了什么地方

let obj = {
  name: 'my name'
};
console.log(obj.__proto__); // { hasOwnProperty: ƒ, …}
console.log(obj.__proto__ === Object.prototype); // true
console.log(obj.__proto__.hasOwnProperty === Object.prototype.hasOwnProperty); // true

那么,如果我手动给一个对象新增一个和原型上同名的方法呢?

let obj = {
  name: 'my name'
};
obj.hasOwnProperty = function (prop) {
  return prop;
};
console.log(obj.hasOwnProperty('name')); // 'name'

毫无疑问,也证明了原型链的概念,优先查找自身属性。或者重写原型上的一个方法在次证明

Object.prototype.hasOwnProperty = function() {
  return 'hasOwnProperty'
}
let obj = {};
console.log(obj.hasOwnProperty()); // 'hasOwnProperty'

这样就解释以下问题了.

  • 数组为什么可以调用mapslice方法
  • 字符串为什么可以调用substrsplit方法
  • 函数为什么可以调用callapply方法

其他实例的原型链,其中stringnumberboolean是通过包装类实现

({}).__proto__ === Object.prototype
([]).__proto__ === Array.prototype
(function(){}).__proto__ === Function.prototype
(/./).__proto__ === RegExp.prototype
('string').__proto__ === String.prototype
(123).__proto__ === Number.prototype
(false).__proto__ === Boolean.prototype
(new Error('msg')).__proto__ === Error.prototype
(new Date()).__proto__ === Date.prototype
(new Set()).__proto__ === Set.prototype
(new Map()).__proto__ === Map.prototype
...

构造函数的原型链

构造函数(除了Object)的prototype.__proto__全部指向Objectprototype

Array.prototype.__proto__ === Object.prototype
Function.prototype.__proto__ === Object.prototype
...

来看下面代码

console.log(Object.prototype.hasOwnProperty.call(Array.prototype, 'valueOf')); // false
let arr = [];
console.log(arr.valueOf()); // []

Object.prototype.hasOwnProperty.call(Array.prototype, 'valueOf')可以看出Array.prototype上并没有valueOf方法,但是由于Array.prototype.__proto__正是Object.prototype。这是原型继承的概念,所以,所有实例(对象)都可以调用Object.prototype下的方法

特殊的Object.prototype

既然其他构造函数的prototype.__proto__都指向Object.prototype,那么Object.prototype.__proto__指向谁呢?答案是null

console.log(Object.prototype.__proto__); // null

四、原型方法 & 静态方法

在ES6之前并没有的概念,但是由于用构造函数去模拟实现‘类’的概念时,就有了从类的概念中搬过来静态方法的这种称呼。原型方法则是挂在构造函数prototype上面的方法

原型方法

一个对象在实例化(new Array 或者 直接字面量)时从构造函数的 prototype 上继承的方法就叫原型方法,也就是说所有原型上面方法都叫原型方法

let arr = [];
arr.map() // map 是从 Array.prototype 上继承的 就称为数组的原型方法

静态方法

静态方法也叫类方法,是在类中是通过static关键字定义的方法,无需实例该类即可直接调用该类的这个方法,就称为静态方法,静态方法不会被继承。那么在js中表现为直接挂在构造函数下的方法就称为静态方法

Object.defineProperty() // defineProperty 就称为 Object 的静态方法

ES6中的静态方法

class Pe{
  static say(){
    return 'my'
  }
}
console.log(Pe.say())

五、ES6中class

ES6 提供了更接近传统语言的写法,引入了 class(类)这个概念,通过class关键字,可以定义一个类

class P {
  constructor() {
    this.name = 'my name';
  }
  getName() {
    return this.name;
  }
}
let p = new P();
console.log(p.getName()); // 'my name'

既然有class,那么对应的继承也不会缺席。在class中是通过extends关键字实现继承。

class P {
  getName() {
    return this.name;
  }
}
class S extends P {
  constructor() {
    super();
    this.name = 's name'
  }
}
let s = new S();
console.log(s.getName()); // 's name'  getName 就是继承过来的方法

这里不是讨论class怎么使用。而是产生一个问题,就是class和之前的构造函数有什么关系呢?class又是怎么是实现继承的呢?

// class
class P {
  constructor() {
    this.name = 'p name';
  }
  getName() {
    return this.name;
  }
}
let p = new P();
console.log(p.getName()); // 'p name'

// 传统构造函数
function S() {
  this.name = 's name';
}
S.prototype.getName = function () {
  return this.name;
};
let s = new S();
console.log(s.getName()); // 's name'

好像有那么点相似的地方,传统构造函数方式的方法是挂在构造函数原型上,实现s实例中可以调用getName方法。而class直接定义在了class里面,那么class是如何实现调用的。先来看看p实例s实例长什么样

class P {
  constructor() {
    this.name = 'p name';
  }
  getName() {
    return this.name;
  }
}
let p = new P();
console.log(p); // {name: "p name", __proto__: {constructor: class P, getName: ƒ}}


function S() {
  this.name = 's name';
}
S.prototype.getName = function () {
  return this.name;
};
let s = new S();
console.log(s); // {name: "s name", __proto__: {constructor: S(), getName: ƒ}}

可以看出使用class实例化出来的实例也有__proto__,那么就可以证明继承其实还是通过原型实现的。下面通过简单的例子证明

class P {
}

class S extends P {
}

console.log(S.prototype.__proto__ === P.prototype);  // true
console.log(P.prototype.__proto__ === Object.prototype); // true
console.log(P.__proto__ === Function.prototype); // true

// 等价于
function PP() {
}
function SS() {
}

SS.prototype = new PP();
SS.prototype.constructor = SS;

console.log(SS.prototype.__proto__ === PP.prototype); // true
console.log(PP.prototype.__proto__ === Object.prototype); // true
console.log(PP.__proto__ === Function.prototype); // true

需要注意的是,子类__proto__指向父类,而构造函数永远指向Function.prototype

class P {
}
class S extends P {
}
console.log(P.__proto__ === Function.prototype); // true
console.log(S.__proto__ === Function.prototype); // false
console.log(S.__proto__ === P); // true


function PP() {
}
function SS() {
}

SS.prototype = new PP();
SS.prototype.constructor = SS;

console.log(SS.__proto__ === Function.prototype); // true
console.log(PP.__proto__ === Function.prototype); // true

六、万物皆对象?

从原型链来看万物皆对象的概念,首先对象最基本的特征是属性,所以可以认为一个数据类型具有属性就可以认为是对象。在日常开发中我们一般看见{}即大括号包起来的才叫对象。其实来讲在JavaScript中大部分都是对象,像数组函数这些是被称为复杂型的对象,包括基本类型stringnumberboolean都属于对象。只不过stringnumberboolean这三个基本类型使用包装类实现。

那么不属于对象的有:

  • undefined
  • null

七、没有__proto__属性的对象

比较特殊的对象有两个:因为它们都没有__proto__属性

  • Object.create(null)
  • Object.prototype

Object.create

Object.createObject下的一个静态方法,该方法接受两个参数,第一个参数可以是一个对象或者null,如果为null将会比较特殊,会没有__proto__这个属性,那么没有这个属性意味着这个对象没有Object.prototype上面的方法

console.log(Object.create(null).__proto__); // undefined