想要搞清楚 原型、原型链、继承 这一堆概念之前首先要搞清楚 对象 是啥
ECMAScript 中的对象其实就是一组数据和功能的集合。 ——— javascript 高级编程设计第三版
对象是一种数据类型,js 中的数据类型分为 原始类型 和 引用类型,原始类型也叫基本类型或者值类型。
原始类型:Undefined、Null、Boolean、Number、String
引用类型:Object
上面提到了 Undefined、Null 这两种数据类型,他俩是比较奇葩的只有一个唯一值的数据类型分别是undefined、null,从逻辑角度来看 null 值表示一个空对象指针,这也正是使用 typeof null 返回object 的原因;而 undefined 是在ECMA 第三版才引入,目的是为了正式区分空对象指针与未初始化(赋值)的变量。
算上es6的 Symbol 也就是7中数据类型,
Array 其实是归在 Object 里面的,Object 又分为:
- Array
- Function
- Date
- RegExp
- Set
- Map
- ...
typeof Symbol() // symbol
typeof [] // object
typeof new Date // object
typeof new Set() // object
typeof function(){} // function
Array、Set、Date 都返回了object,只有 typeof function(){} 返回了function,这是因为Function 本身就是 Object 的一个子类,只不过与普通对象相比,它在内部实现了一个[[call]] 方法,用来表示该对象可以被调用,typeof 在判断一个对象时,如果对象内部包含了[[call]] 方法,就会返回function,也就是说function就是实实在在的对象。
知道对象是什么了,那应该怎么得到一个对象呢?
首先想到的是 new,有同学可能会说我平时 var ()={} 也可以声明一个对象,这种通过字面量的方式是可以声明对象,但这个只是一种快捷方式,程序员们管它叫语法糖,想一想,不管是 RegExp、Array 还是Object 但凡通过字面量可以定义的,new 同样可以,new 才是底层实现。
对于 new 和 字面量这两种定义对象的方式红皮书给出如下解释:
- new 构造函数
- 对象字面量(简写形式,简化包含大量属性的对象创建过程)
既然对象是被 new 出来的,那被 new 的函数是哪来的,哈哈哈,函数的声明也有两种方式”函数式声明“和”函数表达式声明“,区别是函数式声明有函数提升机制。
知道了对象是什么、从哪来,最后再提一下对象怎么用,主要是指怎么调用对象属性,无非通过”点操作符“和”方括号语法“来调用,从功能上看这两种方法没有任何区别,只是方括号语法功能更丰富,它支持下面三个加强功能:
- 通过变量来访问属性
- 属性名中包含会导致语法错误的字符
- 属性名使用了关键字或保留字
var obj = {
name:'n',
'var:'v',
'type name':'t'
}
var name = 'name';
obj[name] // n
obj['var'] // v
obj['type name'] // t
红皮书推荐我们使用点操作符来访问对象属性,除非必须通过变量访问属性的情况
原型
new function(构造函数)
对象是一组数据和功能的集合,function 是对象的子类,
那 function 绝对也有属性比如:
function.name 表示函数名称,
function.length 表示形参长度,
function.prototype 就是原型。
以此为例:
function fun(){};
var obj = new fun()
fun 是一个函数,当它被new 了之后new 出来的obj 就是一个实例对象,这个函数 fun被new了我们就叫它构造函数,
实例 obj 有一个默认属性__proto__,__proto__指向原型,刚才说fun.prototype 也指向原型,
那么 fun.proptotype === obj.__proto__是成立的。
这里插个题外话,当new 一个function 的时候,后面的小括号用来传参,如果没有参数这个小括号可写可不写,比如上面例子中 new fun 和 new fun() 没有任何区别,但是红皮书建议我们不管什么情况new function 后面的小括号都要写上。
现在知道 fun.prototype === obj.__proto__ === 原型,那原型里面有啥呢?可以打印一下 fun.prototype 得到一下结果:
原型是个对象默认有一个 constructor属性,constructor 指向函数本身,也就是说fun.prototypr.constructor === obj.__proto__.constructor === fun。
补充很重要的一点,实例有__proto__属性是因为实例是一个对象,也就是说所有的对象都有一个__proto__属性
new Object (普通对象)
上面说了 自定义函数、实例对象、原型之间的关系,下面看通过字面量声明的对象和原型之间的关系是怎么样的。
var obj1 = {a:1}
var obj2 = new Object();
obj2.a = 1
obj2 是通过 new Object 来得到的,因为实例的__proto__等于构造函数的prototype,所以 obj2.__proto__ === Object.prototype
obj2.__proto__.constructor === Object.prototype.constructor === Object;
obj1 通过字面量的方式声明只是语法糖,底层基于new实现,所以把上面等式的obj2 换成obj1 依然成立,也就是说 obj1、obj2 的原型是同一个东西 obj2__proto__ === obj1.__proto__
function.proto (函数也是对象)
上面提到过所有对象会有一个__proto__属性,而且 function 是对象的子集也是一个对象,那 function.__proto__ 是啥?
在简答这个问题之前要先搞清楚函数是怎么来的,既然对象都是被 new 出来的那么函数也能被new 出来吗?
当然!从这里要开始区分 function 和 Function 了(注意大小写)。
function: 小写 function 是一个关节字用来声明一个函数。
Function: 大写的 Function 是js 的一个内置函数,所有自定义方法都是 Function 的实例。
var fun1 = new Function('x','y','return x + y')
function fun2(){}
这个时候虽然 fun1 是个函数,但 也是个对象,是个被 new 出来的实例所以 fun1.__proto__ === Function.prototype, fun1 换成fun2 依然成立。
既然 Function 可以被 new 说明它是个函数,那函数 Function 又是被谁 new 出来,只能是 Function 它自己,循环了?
是的,这里是个循环结构,实例对象的 __proto__ 等于构造函数的 prototype ,也就是说 Function.__proto__ === Function.prototype。
__proto__ 是实例对象上的属性,prototype 是构造函数上的属性,对于 fun 而言是个例外,fun 是 Function 的实例,同时也可以作为构造函数被 new,所以fun 既有 __proto__ 又有 prototype。
fun.prototype !== fun.__proto__
fun.prototype !== Function.__proto__
fun.__proto__ === Function.__proto__
fun.prototype表示构造函数的原型,这个时候fun看做一个可以被new的构造函数,构造函数的prototype等于实例的__proto__,即 fun.prototype === (new fun).__proto__
,和 fun.__proto__
Function.__proto__
没有关系,等式1、2成立。
fun.__proto__ 表示实例的原型,这个时候 fun 看做 Function 的实例,所以实例的 __proto__ 等于构造参数的 prototype,即
fun.__proto__ === Function.prototype,因为 Function 的特殊性 Function.__proto__ === Function.prototype 所以fun.__proto__ === Function.__proto__ ,等式3 成立。
proto.proto (原型的原型)
简单总结一下上面的内容,对象有一个 __proto__ 指向原型,构造函数有一个 prototype 也是指向原型,原型也是一个对象 那原型的__proto__ 是啥,原型的原型 ?
function fun(){}
var objFun = new fun()
var obj1 = {a:1}
ver obj2 = new Object()
以上面的代码为例 obj1、obj2、Object 他们的原型的原型是null ,
obj1.__proto__ === Object.prototype
obj1.__proto__.__proto__ === null
obj2.__proto__.__proto__ === null
Object.prototype.__proto__ === null
Function 是个函数 Function .prototype 指向的对象也是被 Object 创建的对象所以 Function.prototype.__proto__ === Object.prototype Function.prototype.__proto__.__proto__ === null
原型有向上查找的机制,当到了原型链的顶端就会返回 null ,也就是说原型链的顶端是 null
instanceof (区分 Array 和 Object)
我们通过 typeof 去判断引用类型时返回值是 object 或者 function,这个导致无法有效区分 Array 和 Date 等类型,对于引用类型的区分可以用到instanceof 命令。instanceof 用来判断一个实例是否属于某一个构造函数;
instanceof 的语法很简单 [] instanceof Array ,判断规则是沿着 [].proto 这条线查找看是否和 Array.prototype 这条线存在相同的引用,有就返回true,找到终点 null 还没有发现相同引用就返回false 。
function fun(){}
var obj = new fun()
fun instanceof Function // true
obj instanceof fun // true
obj instanceof Object // true
Array instanceof Function // true
Array instanceof Object // true
[] instanceof Array // true
[] instanceof Object // true
Function instanceof Object // true
new Date instanceof Object // true
new Date instanceof Date // true
Date instanceof Function // true
结合上面讲的内容很容易就能看明白这些 instanceof 的结果,需要注意的是,因为 js 内部全部实例都基于 Object 所以任意实例 instanceof Object 时都会返回true。
实例共享原型方法(原型链)
js 的设计支持实例访问原型上的方法,且构造函数的 prototype 也指向原型,我们只需要把实例公用的方法挂在原型上它的所有实例就可以访问了,比如:
Object.prototype.logObj = function(){ console.log('logObj') }
Object.prototype.logMy = function(){ console.log('logMy_prototype') }
Function.prototype.logFun = function(){ console.log('logFun') }
var obj = {
logMy:function(){ console.log('logMy') }
}
obj.logMy() // logMy
obj.logObj() // logObj
obj.logFun() // err:not a function
function fun(){}
fun.logFun() // logFun
fun.logMy() // logMy_prototype
以 obj.logMy 和 obj.logObj 为例,当一个对象访问方法(或者属性)时,会先从对象自身的私有方法上查找比如 obj.logMy,如果没找到会查找原型上的方法和属性比如 obj.logObj ,如果还没找到会沿着__proto__这条链一直向上找比如 fun.logMy,直到顶端返回null,这就是原型链。
fun.logMy 也是一样的,logMy 属性在Function.prototype 上没有找到的时候,会接着向上查找,因为 Function.prototype 指向一个对象,对象是基于 Object 创建的所以再往下会找到Object.prototype 上面。
我们经常用的数组方法比如 push 、filter,字符串方法 indexOf 等都是这个原理实现的,这样我们自己也可以去扩展一些功能方法了。
hasOwnPrototype (属性在自身还是原型)
既然实例可以共享原型上的属性和方法,那该怎么确定某个属性或者方法到底是实例自身的还是原型上面的呢?js在 Object.prototype 上实现了一个内置方法hasOwnProperty,功能就是区别属性是否来自原型。
Object.prototype.a = 'a'
var obj = {b: 'b'}
obj.hasOwnProperty('a') // false
obj.hasOwnProperty('b') // true
继承
前面七七八八讲了一大堆都是为了给继承做铺垫,继承的方式有很多种后面会讲到,现在先了解一下什么是继承。
举个例子,A班有一百个同学,这一百个同学他们的相同点是都在A班上课、所学课程一样、班主任是同一个人、等等,当然也有不同点比如姓名、年龄、性别、身高、体重、等等,我们用数据来表示这一百个同学要怎么做?如何把相同的东西抽象出来作为父类,每个同学个性化的东西作为子类,子类去继承父类,这样避免了重复代码每个同学的信息就是完整且独立的。现在的问题就抽象成了我们需要一个父类有若干属性和方法,子类也有若干属性和方法,让多个子类拥有父类的属性和方法且相互之间不产生影响,这就是继承。
构造函数实现继承
function fun() {
this.name = 'fun'
}
fun.prototype.myLog = function() { console.log(1) }
function obj() {
fun.call(this)
this.type = 'child'
}
var O = new obj
console.log(O.myLog) // undefined
原理:通过call实现的继承本质是改变了this指向,让父类里面的this指到子类的上下文,这样在父类里面通过this设置的属性或者方法会被写到子类上面。
缺点:只能继承父类构造函数上的属性和方法,不能继承父类原型上的属性和方法。
通过原型链实现继承
function fun() {
this.name = 'fun'
this.arr = [1, 2, 3]
}
fun.prototype.myLog = function() { console.log(1) }
function obj(type) {
this.type = type
}
obj.prototype = new fun()
var O1 = new obj('o1')
var O2 = new obj('o2')
O1.name = 'is O1'
O1.arr.push('123')
console.log(O1.myLog) // 可以继承原型上的属性和方法
console.log(O2.name) // fun
console.log(O2.arr) // [1, 2, 3, '123']
原理:利用原型链向上查找的机制实现继承,给 obj.prototype 赋值为父类的一个实例,当把obj作为构造函数在它的实例O1上查找属性时查找顺序依次是 O1本身 -> obj.prototype(fun实例)-> fun.prototype
这样既能继承父类构造函数上的属性。也能继承父类原型上的属性。
缺点:因为 O1.proto === O2.proto 所以当改变父类构造函数上的属性时O1和O2会相互影响,例子中当改变 O1.arr 时 O2.arr 也跟着变了就是这个原因,而 O1.name 变了 O2.name 没变是因为当设置值时会优先在 O1 自身上查找没有发现 name 属性会在 O1 自身上设置 name 值,这个时候根本没有影响到 proto 上的name。O1 和 O2 上的值不管是自身构造函数上的还是父类构造函数的都应该独立维护相互影响是我们不希望看到的。
构造函数+原型链 实现继承
function fun() {
this.name = 'fun'
this.arr = [1, 2, 3]
}
fun.prototype.myLog = function() { console.log(1) }
function obj () {
fun.call(this)
this.type = 'obj'
}
obj.prototype = new fun()
var O1 = new obj()
var O2 = new obj()
O1.arr.push('123')
console.log(O1.arr) // [1, 2, 3, '123']
console.log(O2.arr) // [1, 2, 3]
原理:通过fun.call(this)改变上下文this指向,父类构造函数上的属性和方法设置到了子类上,相互独立避免影响;通过 obj.prototype = new fun() 实现了继承父类原型上的属性和方法。
缺点:这种方法实现继承,父类构造函数会被执行两次分别在 fun.call(this) 和 obj.prototype = new fun(),而且父类构造函数上的属性在子类自身和子类的原型上都存在,这导致执行了 delete O1.arr
只是删除了O1自身上的arr属性,O1原型上依然存在,根据原型链向上查找机制O1.arr依然可以访问到。
function fun() {
this.name = 'fun'
this.arr = [1, 2, 3]
}
fun.prototype.myLog = function() { console.log(1) }
function obj () {
fun.call(this)
this.type = 'obj'
}
obj.prototype = new fun()
var O1 = new obj()
O1.arr.push('123')
console.log(O1.arr) // [1, 2, 3, "123"]
delete O1.arr
console.log(O1.arr) // [1, 2, 3]
构造函数+原型链 实现继承(优化)
function fun() {
this.name = 'fun'
this.arr = [1, 2, 3]
}
fun.prototype.myLog = function() { console.log(1) }
function obj() {
fun.call(this)
this.type = 'obj'
}
obj.prototype = fun.prototype // 把实例改成了引用解决了上诉问题
var O1 = new fun()
var O2 = new obj()
O1 instanceof obj // true
O2 instanceof obj // true
(new fun()).__proto__.constructor // 父类函数
(new obj()).__proto__.constructor // 父类函数
原理:这个原理就不讲了,上面看明白了这个道理是一样的。
缺点:因为obj.prototype = fun.prototype,导致父类和子类的实例无法做出区分。
Object.create 实现继承
function fun() {
this.name = 'fun'
this.arr = [1, 2, 3]
}
fun.prototype.myLog = function() { console.log(1) }
function obj() {
fun.call(this)
this.type = 'obj'
}
obj.prototype = Object.create(fun.prototype)
obj.prototype.constructor = obj
var O1 = new fun()
var O2 = new obj()
O1 instanceof obj // false
O2 instanceof obj // true
(new fun()).__proto__.constructor // 父类函数 fun()
(new obj()).__proto__.constructor // 子类函数 obj()
原理:通过create函数创建中间对象,把两个对象区分开,因为通过create创建的对象,原型就是create函数的参数。
优点:实现了继承,实现了父子类隔离。
同时继承多个对象
function fun1() {
this.name1 = 'fun1'
this.arr1 = [1, 2, 3]
}
fun1.prototype.myLog1 = function() { console.log(1) }
function fun2() {
this.name2 = 'fun2'
this.arr2 = [11, 22, 33]
}
fun2.prototype.myLog2 = function() { console.log(2) }
function obj() {
fun1.call(this)
fun2.call(this)
this.type = 'obj'
}
obj.prototype = Object.assign(obj.prototype, fun1.prototype, fun2.prototype)
obj.prototype.constructor = obj
var O = new obj()
Object.assign(target, ...sources):该方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,它将返回目标对象。
class
class 是es6新增的,我们看如何用class来搞一个对象以及实现继承。
class搞一个对象
function Student(name) {
this.teacher = '王老师'
this.name = name
}
Student.prototype.hello = function () {
console.log(`我是${this.name},我的老师是${this.teacher}。`)
}
var xiaoming = new Student('小明')
var xiaohong = new Student('小红')
class Student {
constructor(name) { // 构造函数
this.teacher = '王老师'
this.name = name
}
hello() { // 定义在原型对象上的函数
console.log(`我是${this.name},我的老师是${this.teacher}。`)
}
}
var xiaoming = new Student('小明')
var xiaohong = new Student('小红')
通过class定义的类需要实例化出对象的时候也需要new,这和前面说的对象都是new出来的相对应,区别在于通过class关键字定义类代码更简洁,避免了挂载prototype这种分散的代码。
class继承
class Base {
constructor(name) {
this.name = name
this.school = 'xx大学'
this.course = ['语文', '数学']
this.teacher = '王老师'
}
modifyTeacher(tName) {
this.teacher = tName
}
}
class Student extends Base {
constructor(name) {
super(name)
this.time = new Date()
}
addCourse(course) {
this.course.push(course)
}
}
var xiaoming = new Student('小明')
var xiaohong = new Student('小红')
extends:extends关节字用来继承一个父类,子类拥有父类的属性和方法。(extends表示原型链对象来自Base)。super():super用来调用父类的构造函数,否则父类的name属性无法正常初始化。
通过 extends 关节字就实现了继承,比通过原型链实现代码清爽了许多,xiaoming 和 xiaohong 这两个实例上拥有属性 time、name、school、course、teacher,拥有方法 modifyTeacher、addCourse,且互不影响。
ES6引入的class和原有的JavaScript原型继承有什么区别呢?实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码。