原型和原型链
考察 js 基础知识,原型和原型链必考的内容,如果你技术面试没有被考到原型和原型链,只能有两种情况下。第一种情况面试官比较蠢,他不知道这个基础知识,这也有可能,因为面试官说不定就是公司比较底层的员工,他有可能就忽略了这种基础内容。第二种可能基础知识题比较多 时间原因 抽取一部分给你考察,正好没抽到原型和原型链
JavaScript 本身是基于原型继承的语言,我们在 es6 之前,写继承的时候 只能通过原型来继承,不像 java 可以通过 class。从 es6 之后,引入了 class 语法 我们可以通过 class 来继承,但是基于我们现在 class 的剖解来看,class 也仅仅只是形式上的继承的一个写法而已,它真正的继承还是一个原型上的继承,我现在说的这一点,在接下来会为大家一一去验证解答,还有一点讲的就是说,我们现在 es6 已经普及了,我就以 es6 的 class 从头开始讲起,我们就不再讲 es5 的那种没有 es6 的构造函数的那种继承的方式了,所以我们就以 es6 为主,顺应历史方向
class 和 继承
constructor
属性
方法
// 类
class Student {
// 大写字母开头
constructor(name, number) {
// constructor一个模板
this.name = name; // 属性赋值给this this的意思就是当前你正在构建的这个实例
this.number = number;
// this.gender = "male" // 没有参数也可以对属性赋默认值
}
sayHi() {
// 增加一个方法 sayHi
console.log(`姓名 ${this.name}, 学号 ${this.number}`);
}
}
// 通过类 new 对象/实例
const xialuo = new Student("xialuo", 100);
console.log(xialuo.name); // xialuo
console.log(xialuo.number); // 100
xialuo.sayHi(); // 姓名 xialuo 学号 100
// 我们可以申明无数个 只要你的内存允许
const madongmei = new Student("madongmei", 101);
console.log(madongmei.name); // madongmei
console.log(madongmei.number); // 101
madongmei.sayHi(); // 姓名 madongmei 学号 101
继承 extends
super 执行父类的构造函数(父类的构建过程)
扩展或重写方法
// 父类
class People {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} eat something`);
}
}
// 子类
class Student extends People {
constructor(name, number) {
super(name);
this.number = number;
}
sayHi() {
console.log(`姓名 ${this.name} , 学号:${this.number}`);
}
}
// 子类
class Teacher extends People {
constructor(name, major) {
super(name);
this.major = major;
}
teach() {
console.log(`${this.name} 教授 ${this.major}`);
}
}
// 学生实例
const xialuo = new Student("xialuo", 100);
console.log(xialuo.name);
console.log(xialuo.number);
xialuo.sayHi();
xialuo.eat();
// 老师实例
const wanglaoshi = new Teacher("王老师", "语文");
console.log(wanglaoshi.name);
console.log(wanglaoshi.major);
wanglaoshi.teach();
wanglaoshi.eat();
类型判断 instanceof
// 可以在上面的代码中去演示一下
xialuo instanceof Student // true
xialuo instanceof People //true
xialuo instanceof Object //true
[] instanceof Array //true
[] instanceof Object //true
{} instanceof Object //true
我们可以通过 instanceof 去判断到底属于哪个 class 或 属于哪个构造函数,比如说我们之前写过数组可以通过 instanceof 判断它是 Array 这个方式来判断它是不是数组,这个意思就是说 我们要看一下这个变量是不是这个 Array 这个 class 来构建出来的,跟这个 xialuo instanceof Student 等于 true 是一样的,也就是说这个变量是这个 class 构建出来的 就是 true
我们还可以通过xialuo instanceof People //true,这个 People 是 Student 的父类 xialuo 是 Student 构建出来的 但是 People 是它的父类,People 也参与了构建 xialuo 的一部分,比如 eat name 都是 People 提供的,所以它是 true。xialuo instanceof Object //true也是 true 因为 Object 是所有 class 的一个父类,你可以这么认为,People 是 Student 的父类,Object 是 People 的父类 ,这个不用我们自己去写,js 引擎就会这么做,所以说这种的引用类型 Object 都是 true。包括数组和对象。People instanceof Student 等于 false People 并不是 大家可以看看 People 第一个 class 的例子 People 不是直接继承 Object 为什么这么说呢 是 Student 先继承于 People 再继承给 Object 大家可以看一下我们在第一个 class 的示例,那个时候还没有 People,当时只有一个 Student ,那个时候如果只有一个 Student 没有 People 的话 你可以说 Student 继承于 Object,但是后来第二个示例 People 从中间插了一脚,告诉 Student 你不要继承 Object 你先继承给我,这样的话 Student 就继承于 People,然后 People 又自己默认继承于 Object,所以说它是一连串的关系。这始终是一个表现的说法,一个规则的论述。接下来我们通过原型深入理解 instanceof 到底是怎么回事。讲原型的目的并不是为了讲 instanceof,而是原型本身就很重要,顺便讲完原型之后再去看看 instanceof 的规则,你应该就能马上明白了。
原型
此处比较难理解,大家一定要认真看!
// class实际上是函数,可见是语法糖
typeof People; // 'function'
typeof Student; // 'function'
// 隐式原型和显示原型
console.log(xialuo.__proto__);
console.log(Student.prototype);
console.log(xialuo.__proto__ === Student.prototype);
首先 typeof Student 是什么,我们可以基于第二个示例的代码在浏览器中看一下 它是 function ,typeof People 也是 function ,class 只是一个语法糖,大家可以看到 class 的本质还是一个 function,只是写法是 class 的写法,然后我们再看一点,xialuo 有 xialuo.name, xialuo.number, xialuo.sayHi() 。还有一点 xialuo 有__proto__,这个是 js 引擎帮我们设的,并不是我们自己写的

大家可以看到这里也有一个 sayHi constructor 先不用管 然后我们获取这个 sayHi 来执行

这个时候为什么是 undefined 呢?因为我们的作用域发生了变化,作用域的问题先不管,后面会讲为什么是 undefined
我们再看一个 Student 也有一个 sayHi

那我们再看一个
Student.prototype === xialuo.__proto__; // true
我们发现一个很有意思的现象,我们首先探知了两个属性 第一个叫__proto__ 第二个叫prototype.我们先给他们取个名字,两个下划线的叫隐式原型,prototype 我们叫显示原型,然后 xialuo 的隐式原型等于 Student 的显示原型,而且是全等,意思就是说他们两个的引用类型引用的是一个地址,大家可以看看之前的堆栈模型图。
我们接下来看原型图

它表述的意思和上面的代码是一个意思,xialuo 有 name 有 number 还有一个__proto__,指向了 Student 的显示原型下面的 sayHi ,其实这个东西就是 Student 的 prototype ,这个意思就是说我们在定义 class 的时候 它会有一个 prototype 也就是显示原型指向这样的一个对象 然后会把方法都放在这里面来,通过 new Student 申明出一个 xialuo 之后 xialuo 的 name 和 number 直接放在属性对象本身,然后 sayHi 是通过__proto__隐式原型来去指向指向 Student.prototype 来获取的,所以说 根据这个图,我们能看出规则
原型关系
每个 class 都有显示原型 prototype
每个实例都有隐式原型__proto__
实例的 __proto__指向对应 class 的 prototype
基于原型的执行规则
获取属性 xialuo.name 或执行方法 xialuo.sayHi() 时
先找自身属性和方法寻找
比如 name 就是自身的属性,xialuo,sayHi 就不是自身的属性,所以说这个时候呢我们要进行第二步
找不到则自动去__proto__中查找
我们根据这个规则,再去看上面的图,我们获取 xialuo.name,直接从 xialuo 里面去寻找了,xialuo 本身就有这个 name。我们如果 xialuo.sayHi 本身找不到,就自动去隐式原型中寻找,而隐式原型正好指向 class 的显示原型,就找到了 sayHi。
这就是原型,还没有到原型链,还没有链起来,原型最主要就是那个图,对应之前的代码详细看
原型链
console.log(Student.prototype.__proto__);
console.log(People.prototype);
console.log(People.prototype === Student.prototype.__proto__);
有了原型之后,我们可以继续讲解原型链,原型链我们就得请出另外一个 class 那就是 People,我们知道了每个 class 都有显示原型,所以我们打印出 People.prototype,问题在于什么地方呢 我们又引出了一个东西叫做Student.prototype.__proto__ Student 的显示原型的隐式原型,它正好等于 People 的显示原型(People.prototype),他们两个是一个继承关系 Student 是继承 People 的,所以说 Student 的原型它有一个隐式原型正好挂在 People 的显示原型。
这个时候千万不要被绕懵了,请看下图

首先 左下角三个部分大家都知道了,就是我们上面讲的那张图,然后再往上看,Student 的原型它又有一个隐式原型指向了 People 的原型 People 的原型里面有 eat 方法,那逻辑也是一样的。xialuo 它是通过 Student 给 new 出来的,所以 xialuo 的隐式原型指向 Student 的显示原型,三角关系。然后你也可以理解为 Student 的原型,也就是说 class 的原型的本身也是一个普通的对象,它没有什么黑科技,没有什么技巧,就是个普通的对象,它里面有个方法叫 sayHi,这个对象你也可以理解为它是通过 People 给 new 出来的 当然这个过程不是说显示的你 new 一个 People 怎么怎么样,这个过程是一个可以这么理解为的,或者是 js 引擎内部做的,不用我们关心,你可以理解为 Student 是通过 People new 出来的,所以它的隐式原型指向 People 的显示原型,正好是这么一个关系。
用这个图就能理解我们上面的三行代码,就是 Student 的原型有个隐式原型,然后 People 有显示原型,然后 Student.prototype 的隐式原型等于 People 的显示原型,我们可以试一下。


我们屡一下,我们访问 xialuo.name 是直接获取本身的,我们访问 xialuo.sayHi,因为它本身没有 sayHi,所以说我们通过__proto__隐式原型到 Student 的显示原型(Student.prototype)访问的 sayHi,再往下,我们访问 xialuo.eat ,Student 的显示原型(Student.prototype)里面并没有 eat 这个方法,它就会自动再去隐式原型里面往上找。在每一级,它都会去访问当前的属性,如果它没有的话它就会去找他的隐式原型,最后形成一个链。
我们通过图中可以看到 name 是 xialuo 本身的一个属性,sayHi 不是它本身的属性,那这个图怎么去验证呢,我们可以通过 hasOwnProperty,看下图

我们发现 sayHI 是不存在的 但是我们可以访问 sayHi 这个方法,hasOwnProperty 可以看到是不是自己的属性。
现在可以去看深拷贝的代码,学完这个东西,你就知道当时我在写深拷贝的时候为什么要加 hasOwnProperty。
问题又来了 hasOwnProperty 是从哪来的呢?
hasOwnProperty 也是 xialuo 身上的一个方法,但是 不是它自己的属性,OK 继续往下

这个时候又多了一层,下面的五个都讲过了,我们说过 Student 继承于 People, People 继承于 Object ,所以说 People 的属性里面也有隐式原型指向 Object 的一个原型,Object 原型是 js 引擎本身自己有的,那 Object 原型里面有什么呢,有 toString,有 hasOwnProperty ,当然 还有很多没有列举,所以我这个 xialuo.hasOwnProperty,如果访问这个方法就会通过这个原型链一直往上找,找到 Object 的原型上面去,就形成了一个很明显的原型链。那能不能再顺着 Object 往上找呢,不好意思,Object 的隐式原型指向 null,到此为止 就拉倒了,如果再不拉倒就很乱了,现在本来就很乱了。所以说,原型链就是这个样子,这就是 class 里面调用方法,调用属性的一个本质。以及继承的本质,包括是如何继承与 Object 的一个本质。我强烈要求大家把代码写出来,把图画出来,不要参考任何东西,就拿一张白纸画,最基本的要求。
还是比较有难度的,希望大家认真吸收,好好理解
OK 这个图不变 再看 instanceof
instanceof
xialuo instanceof Student;
xialuo instanceof People;
xialuo instanceof Object;
就拿这一层来说,instanceof 到底是什么工作原理。xialuo instanceof Student ,instanceof 前面的这个变量顺着隐式原型往上一直找,然后能不能找到第二个操作数,也就是 Student 的这个显示原型或者 People 的显示原型或者 Object 的显示原型 ,比如说 Student 和 People 和 Object 的显示原型在这里摆着, xialuo 顺着隐式原型一层一层往上找,能不能对应到 class 的显示原型,如果能对应到,instanceof 成立,如果对应不到 instanceof 就不成立 返回 false,比如xialuo instanceof Array 肯定返回一个 false。数组的原型不在体系内,数组的原型可能在其他的地方,所以说 xialuo 顺着隐式原型找不到数组的显示原型,所以返回 false。到此大家应该明白 instanceof 什么意思了吧。
重要提示!!!
class 是 ES6 语法规范,由 ECMA 委员会发布
ECMA 只规定语法规则,即我们代码的书写规范,不规定如何实现
以上实现方式都是 V8 引擎的实现方式,也是主流的
如何准确判断一个变量是不是数组?
a instanceof Array; // 结合原型链图理解
手写一个简易的 jQuery,考虑插件和扩展性。
jQuery 不是很常用,但是要学习 jQuery 的设计方式是我们学习 js 基础知识和原型这部分非常非常非常好的一种形式。用到刚刚讲解的 class 来写就比较简单了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<p>一段文字 1</p>
<p>一段文字 2</p>
<p>一段文字 3</p>
<body>
<script src="./jquerydemo.js"></script>
</body>
</html>
// jquerydemo.js
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];
fn(elem);
}
}
on(type, fn) {
return this.each((elem) => {
elem.addEventListener(type, fn, false);
});
}
// 还扩展很多 DOM API
}
const $p = new jQuery("p");
$p.get(1);
$p.each((elem) => {
console.log(elem.nodeName);
});
$p.on("click", () => alert("clicked"));
扩展性(两种方式)
// 插件
jQuery.prototype.dialog = function (info) {
alert(info);
};
// 造轮子
class myJQuery extends jQuery {
constructor(selector) {
super(selector); // 拿到父类所有的属性和方法
}
// 扩展自己的方法
addClass(className) {}
style(data) {}
}
class 的原型本质,怎么理解?
原型和原型链的图示
属性和方法的执行规则