JavaScript基础篇(二)基于ES6语法再看原型和原型链

原型和原型链真的是 JavaScript 面试中永远绕不开的话题,说实话,感觉理解起来其实也不复杂,但是吃透或者实际应用中做到活学活用又感觉总是差那么一点意思?两年前自己写过相关文章:关于实例、构造函数、原型、原型链内容整理 ,不过当时是用 ES5 写的,如今 ES5 快要成为过去式了,正好用 ES6 中的语法重新梳理一遍,加深自己的印象。

认识 Class


class 语法其实说白了就是一个语法糖而已,其底层还是通过构造函数来创建的。ES6 中的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。我们先来用 ES5 的语法写一个构造函数

function student(name, number) {
  this.name = name
  this.number = number
}
student.prototype.sayHi = function () {
  console.log(`姓名:${this.name},学号:${this.number}`)
}
let zhangsan = new student('张三', 100)
console.log(zhangsan.name) // 张三
console.log(zhangsan.number) // 100
console.log(zhangsan.sayHi()) // 姓名:张三,学号:100

当然,ES5 这种我估计大家都快写烂了,我们用 ES6 语法来将上述代码重写一遍

// 创建类
class Student {
  constructor(name, number) {
    this.name = name
    this.number = number
  }
  sayHi() {
    console.log(
      `姓名:${this.name},学号:${this.number}`
    )
  }
}
// 通过类 new 对象/实例
let zhangsan = new Student('张三', 100)
console.log(zhangsan.name) // 张三
console.log(zhangsan.number) // 100
console.log(zhangsan.sayHi()) // 姓名:张三,学号:100

注意:我们在用 class 定义类的时候,前面不需要加 function, 类名的第一个首字母一般都会大写,而且方法之间不需要逗号分隔,加了会报错。

我们首先来看一下 Student 类中的第一个方法 constructorconstructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。方法中的 this 关键字代表实例对象, 类的数据类型就是函数,前面我们也说了,其实 class 的写法本身就是 ES5 中构造函数写法的语法糖,类本身就指向构造函数。如下栗子:

class A {
}
// 等同于
class A {
  constructor() {
  }
}
console.log((typeof A)); // function

先不扯远了,毕竟这里我们主要记录的还是原型和原型链,关于 ES6class 类的定义和一些用法后期会专门记录一篇文章深入理解。回到我们上面的 Student 类和实例 zhangsan 中来。

我们前面说过很多次,class 的写法本身就是 ES5 中构造函数写法的语法糖,所以我们在 Student 类当中定义的 sayHi 方法其实等同于 ES5 中直接在原型上添加的方法:

// ES5 通过 prototype 添加方法
student.prototype.sayHi = function () {
  console.log(`姓名:${this.name},学号:${this.number}`)
}
// ES6 类中方法的定义
class Student {
  sayHi() {
    console.log(
      `姓名:${this.name},学号:${this.number}`
    )
  }
}

那么我们在这里可以打印出 Student 类的 prototype

console.log(Student.prototype) // {constructor: ƒ, sayHi: ƒ}

接下来我们再来看一下实例 zhangsan__proto__

console.log(zhangsan.__proto__) // {constructor: ƒ, sayHi: ƒ}

好吧,奇怪的化学反应好像发生了,实例的 __proto__ 好像和类的 prototype 好像完全相等,用我们的 === 验证一下

console.log(Student.prototype === zhangsan.__proto__) // true

那么问题来了,prototype__proto__ 是什么鬼?为啥这两者会相等?说了半天你也没告诉我原型是什么,只给了我 3 段莫名其妙的代码让我自己揣摩是吧!!!

  • prototype显示原型。我们可以这样理解,每个 class 都有显示原型。无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。普通对象没有 prototype,但有 __proto__ 属性。这个类中定义的一些方法都存储在这个类的 prototype。如下图:

    01.png

  • __proto__隐式原型 。每个实例都有隐式原型,实例的隐式原型指向 class 的显示原型,具体关系如下图:

    02.png

基于原型的执行规则
  • 获取属性 zhangsan.name 或执行方法 zhangsan.sayHi()
  • 先在自身属性和方法寻找
  • 如果找不到则自动去 __proto__ 中查找

好吧,感觉绕了一圈就是想引出实例的隐式原型等于类的显示原型呗,实例上不存在的方法,会通过隐式原型找到类的显示原型中去,如果类的显示原型中存在该方法,那么实例就可以直接调用到该方法。那么问题来了,实例可以通过 _ proto _ 指向原型对象,构造函数可以通过 prototype 指向原型对象,那么原型对象是否有属性指向构造函数或者实例呢?

原型对象无法直接指向实例,因为同一个类可以生成多个实例,但是原型对象指向构造函数倒是有,这里就引入了 constructor ,每个原型对象都有一个 constructor 属性指向关联的构造函数。

console.log(Student.prototype.constructor === Student) // true

所以我们的关系图又可以更新啦:


03.png

好吧,休整休整,通过一系列的证明和画图,我们总算是初步罗列了类、实例、原型对象之间的基本关系。再啰嗦的汇个总:

每一个类都有自己的显示原型 prototype,类中的方法基本都存储在原型对象(类的 prototype )中;实例通过类生成,每一个实例都有自己的隐式原型 __proto__,并且实例的隐式原型等于类的显示原型,所以实例调用的方法如果自身找不到就会找到类的显示原型中;同时类的显示原型通过 constructor 也可以得到类本身。

认识原型链


感觉原型就说了一大堆,其实除了有点绕,感觉真心不复杂!!!原型链字面意识上来讲就是原型组成的链吗,那么我们就通过类的继承先来搭建一条链(声明父类,多创建几个具有层级关系的子类)

// 构建父类 People,所有子类继承 name 属性和 eat 方法
class People {
  constructor(name) {
    this.name = name
  }
  eat(food) {
    console.log(`${this.name}爱吃:${food}`)
  }
}
// 构建子类 Student
class Student extends People {
  constructor(name, number) {
    super(name)
    this.number = number
  }
  sayHi() {
    console.log( `姓名:${this.name},学号:${this.number}`)
  }
}
// 构建子类 Teacher
class Teacher extends People {
  constructor(name, major) {
    super(name)
    this.major = major
  }
  teach() {
    console.log(`${this.name}教${this.major}`)
  }
}
// 根据 Student 类构建实例对象 zhangsan
const zhangsan = new Student('张三', 100)
console.log(zhangsan.name) // 张三
console.log(zhangsan.number) // 100
zhangsan.sayHi() // 姓名:张三,学号:100
zhangsan.eat('土豆') // 张三爱吃土豆
// 根据 Teacher 类构建实例对象 wanglaoshi
const wanglaoshi = new Teacher('王老师', '语文')
console.log(wanglaoshi.name) // 王老师
console.log(wanglaoshi.major) // 语文
wanglaoshi.teach() // 王老师教语文
wanglaoshi.eat('大蒜') // 王老师爱吃大蒜

又出现了两个奇怪的单词 extendssuper,其实都和继承相关,extends 很容易就能看出是构建继承关系,super 在这里表示父类的构造函数,用来新建父类的 this 对象。子类必须在 constructor 方法中调用 super 方法,否则新建实例就会报错。这是因为子类自己的 this 对象,必须先通过 父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。这里就不展开代码罗列了,简单了解概念就行。毕竟 class 写法属于 ES6 语法部分,其实它的用法也比较多,可能需要单独来记录一篇。

因为 Student 类继承于 People 类,前面说原型的时候也提到了,每一个类创建就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象,而每一个函数对象都有 __proto__属性,那么我们先基于这些理论来构建子类 Student 和父类 People的初步关系:

console.log(Student.prototype.__proto__ === People.prototype) // true

算了,看这段代码估计小伙伴也懵逼,还是继续画流程图,用图说话:

04.png

从图中其实我们可以将 Student 也理解成是通过 new People 生成的一个实例对象(仅供理解,可能JS底层解析是这样渲染的,当参考就行...)。zhangsan 这个实例对象拥有 namenumber 两个自身属性。而 sayHi() 其实来自于 Student 类,eat() 其实来自于 People 类,怎么去验证这件事情了?基础篇(一)中我们写深拷贝函数的时候用到过 hasOwnProperty 这个方法,当时说了这个方法主要是验证该对象的属性是自身属性还是通过原型 prototype 添加的属性,那我们在这里在来尝试一下:

console.log(zhangsan.hasOwnProperty('name')) // true
console.log(zhangsan.hasOwnProperty('number')) // true
console.log(zhangsan.hasOwnProperty('sayHi')) // false
console.log(zhangsan.hasOwnProperty('eat')) // false

奇怪的问题又来了,hasOwnProperty 又从哪里来的呢,为啥 zhangsan 可以对它进行直接调用呢?我们要不要验证一下 zhangsan.hasOwnProperty('hasOwnProperty') 来看看,不用说肯定是 false,那么说明 zhangsan 肯定是在哪里继承了这个方法才能直接使用这个方法,很明显 PeopleStudent 两个类中都没有这个方法,那它到底来自哪里呢?看图解答:

image.png

原来 People 类的上一级还有一个大Boss,那就是 Object,也就是说 People 的显示原型对象的隐式原型指向了 Object 的显示原型,而 Object 的显示原型中包含了一系列底层帮我们定义好的方法:toStringhasOwnProperty 等,所以 zhangsan 才可以直接调用 hasOwnProperty 这个方法。在这里我们要知道 Object.prototype 已经是所有原型的最顶层了,它的隐式原型永远指向 null ,也就是不会再往上找了。这些原型所形成的的闭环其实就是原型链,

总结原型链的概念

简单理解原型链就是原型组成的链,实例的 _ proto _ 是一个原型对象,既然它是一个原型对象也有 _ proto _ 属性,原型 _ proto _ 又是原型的原型,我们就可以一直通过_ proto _向上找,这就是原型链。当向上找到Object的原型的时候,这条原型链就算到头了。通过原型链,实例对象可以一层一层往上找不属于自身定义的属性或者方法并直接调用它们。

基于原型链再看 instanceof

instanceof 的原理其实就是通过隐式原型 __proto__ 去原型链中向上查找,如果该实例的隐式原型能在原型链上找到对应类的显示原型,那么就说明它们有继承关系即返回 true,如果找不到即会返回 false

扩展思考

学而不用基本等于没用,总结了原型和原型链这一大堆内容,我们总要用起来才能加深我们对知识的理解!题目来了,手写一个简易的 jQuery 并且要考虑插件和扩展性?

<body>
<p>111</p>
<p>222</p>
<p>333</p>
</body>
<script>
class jQuery {
  // 获取元素节点
  constructor(selector) {
    const result = document.querySelectorAll(selector)
    const length = result.length
    for (let i = 0; i < length; i++) {
      this[i] = result[i]
    }
    this.length = length
    this.selector = selector
  }
  // 根据索引获取对应元素
  get(index) {
    return this[index]
  }
  // 遍历
  each(fn) {
    for (let i = 0; i < this.length; i++) {
      const elem = this[i]
      const index = i
      fn(elem, index)
    }
  }
  // 绑定事件
  on(type, fn) {
    return this.each(elem => {
      elem.addEventListener(type, fn, false)
    })
  }
}
// 验证我们写的函数
let $p = new jQuery('p')
console.log($p) // jQuery {0: p, 1: p, 2: p, 3: p, length: 4, selector: "p"}
$p.get(0) // <p>111</p>
$p.each((item, index) => {
    console.log(item, index) // <p>111</p> 0 <p>222</p> 1 <p>333</p> 2
  })
$p.on('click', function () {
  console.log(111)
})
</script>

初步验证,我们上面写的四个简单的方法都可以用,接下来我们要考虑如何给这个简单的 jQuery 来装上插件,这里我们假设写一个弹框插件:

jQuery.prototype.dialog = function (info) {
  alert(info)
}
let $p = new jQuery('p')
$p.on('click', function () {
  $p.dialog('我是弹窗')
})

好吧,一般写插件其实就是在它的原型上增加一些方法而已,那如果我们想基于这个 jQuery 自己扩展写一下方法呢?比如我这里想增加一个添加类名的方法

class myJquery extends jQuery {
  constructor(selector) {
    super(selector)
  }
  // 假设接收两个参数,第一个是类名,第二个是索引(不传默认所有的都加)
  addClass(className, index) {
    const result = document.querySelectorAll(this.selector)
    // 判断第二个参数是否存在
    if (index && typeof index === 'number') {
      result[index].className = className
    } else {
      // 不存在所有元素都加上
      for (let i = 0; i < this.length; i++) {
        result[i].className = className
      }
    }
  }
}
let $p = new myJquery('p')
$p.addClass('aaa', 1)
$p.addClass('aaa')

好吧,写的很简单,实现其实也不难,更多的是思维发散。原型和原型链就整理到这里了,看起来简单实际整理确也用了好长时间,不过好记性不如烂笔头,多写相信终归是有用的。说实话自己只是个小菜鸟,如果文中有不对的地方或者理解有误的地方欢迎大家提出并指正。每一天都要相对前一天进步一点,加油!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351