前言
在JavaScript中对象是一个复杂又简单的概念,而原型链和对象的关系又是紧密的联系的。但是原型链本身又是复杂的,也是JavaScript中继承的基本条件
一、构造函数
构造函数有点像其他语言的类,可以使用new
运算符可以创建一个实例,构造函数分两类:
- 内置
Object
、Array
、Function
、Date
、Map
和Set
等等
- 自定义
- 被
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'
这样就解释以下问题了.
- 数组为什么可以调用
map
、slice
方法 - 字符串为什么可以调用
substr
、split
方法 - 函数为什么可以调用
call
、apply
方法 - …
其他实例的原型链,其中string
、number
和boolean
是通过包装类实现
({}).__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__
全部指向Object
的prototype
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中大部分都是对象,像数组函数这些是被称为复杂型的对象,包括基本类型string
、number
和boolean
都属于对象。只不过string
、number
和boolean
这三个基本类型使用包装类实现。
那么不属于对象的有:
- undefined
- null
七、没有__proto__属性的对象
比较特殊的对象有两个:因为它们都没有__proto__
属性
- Object.create(null)
- Object.prototype
Object.create
Object.create
是Object
下的一个静态方法,该方法接受两个参数,第一个参数可以是一个对象或者null
,如果为null
将会比较特殊,会没有__proto__
这个属性,那么没有这个属性意味着这个对象没有Object.prototype
上面的方法
console.log(Object.create(null).__proto__); // undefined