阅读 7723
搞懂JS 对象、原型、继承  🦒🐘🐅

搞懂JS 对象、原型、继承 🦒🐘🐅

前言

大家都是抱着找对象的目的进来的,但是本文会从对象开始尝试把,原型、new、构造函数、instanceof、原型链、继承、class 这些有关联的东西都梳理一遍,大家有不同的观点一定要点赞 ~

对象

想要搞清楚原型、原型链、继承这一堆概念之前首先要搞清楚对象是啥

ECMAScript中的对象其实就是一组数据和功能的集合。 —— javascript高级程序设计第三版

红皮书给出的对象解释相当到位

对象是一种数据类型,js中的数据类型分为 原始类型引用类型,原始类型也叫基本类型或值类型。

原始类型:Undefined、Null、Boolean、Number、String
引用类型:Object

上面提到了 Undefined、Null 这两种数据类型,他俩是比较奇葩的只有一个唯一值的数据类型值分别是 undefined、null,从逻辑角度来看 null 值表示一个空对象指针,这也正是使用 typeof null 时返回 object 的原因;而 undefined 是在ECMA第三版才引入,目的是为了正式区分空对象指针与未初始化(赋值)的变量。

算上es6的 Symbol 也就7种数据类型,有同学要问了 Array 和 Set Map 被吃了?

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 O={} 也可以声明一个对象,这种通过通过字面量的方式是可以声明对象,但这个只是一种快捷方式程序员们管它叫语法糖,想一想,不管是 RegExp、Array 还是 Object 但凡通过字面量可以定义的,new同样可以,new才是底层实现。

对于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.prototype === obj.__proto__ 是成立的。

这里插个题外话,当new一个function的时候,后面的小括号用来传参,如果没有参数这个小括号可写可不写,比如上面例子中 new funnew fun() 没有任何区别,但是红皮书建议我们不管什么情况new function时后面的小括号都要写上。

现在知道 fun.prototype === obj.__proto__ === 原型,那原型里面有啥呢?大家可以 console.log(fun.prototype) 出来看一下我就不截图了,原型是个对象默认有一个 constructor 属性,constructor指向函数本身,也就是说 fun.prototype.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是构造函数上的属性,对于fun1而言是个例外,fun1是Function的实例,同时也可以作为构造函数被new,所以fun1既有__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}
var 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 等都是这个原理实现的,这样我们自己也可以去扩展一些功能方法了。

hasOwnProperty(属性在自身还是原型)

既然实例可以共享原型上的属性和方法,那该怎么确定某个属性或者方法到底是实例自身的还是原型上面的呢?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函数的参数。

优点:实现了继承,实现了父子类隔离。

Object.assign(target, source, ...) 将所有可枚举属性的值从一个或多个源对象分配到目标对象;并返回目标对象。可用于做对象拷贝,当目标对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法在二级属性以后就是浅拷贝。

同时继承多个对象

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的好处就是极大地简化了原型链代码。

文章分类
前端
文章标签