JavaScript 原型链和继承问题

JavaScript 中没有类的概念的,主要通过原型链来实现继承。通常情况下,继承意味着复制操作,然而 JavaScript默认并不会复制对象的属性,相反,JavaScript只是在两个对象之间创建一个关联(原型对象指针),这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

原型

  • 当我们 new 了一个新的对象实例,明明什么都没有做,就直接可以访问 toString 、valueOf 等原生方法。那么这些方法是从哪里来的呢?答案就是原型。
  • 在控制台打印一个空对象时,我们可以看到,有很多方法,已经“初始化”挂载在内置的 proto 对象上了。这个内置的 proto 是一个指向原型对象的指针,它会在创建一个新的引用类型对象时(显示或者隐式)自动创建,并挂载到新实例上。当我们尝试访问实例对象上的某一属性 / 方法时,如果实例对象上有该属性 / 方法时,就返回实例属性 / 方法,如果没有,就去 proto 指向的原型对象上查找对应的属性 / 方法。这就是为什么我们尝试访问空对象的 toString 和 valueOf 等方法依旧能访问到的原因,JavaScript 正式以这种方式为基础来实现继承的。

构造函数

如果说实例的 proto 只是一个指向原型对象的指针,那就说明在此之前原型对象就已经创建了,那么原型对象是什么时候被创建的呢?这就要引入构造函数的概念。
其实构造函数也就只是一个普通的函数而已,如果这个函数可以使用 new 关键字来创建它的实例对象,那么我们就把这种函数称为 构造函数

// 普通函数
function person () {}// 构造函数,函数首字母通常大写
function Person () {}
const person = new Person();
  • 原型对象正是在构造函数被声明时一同创建的。构造函数被申明时,原型对象也一同完成创建,然后挂载到构造函数的 prototype 属性上:


原型对象被创建时,会自动生成一个 constructor 属性,指向创建它的构造函数。这样它俩的关系就被紧密地关联起来了。

细心的话,你可能会发现,原型对象也有自己的 proto ,这也不奇怪,毕竟万物皆对象嘛。原型对象的 proto 指向的是 Object.prototype。那么 Object.prototype.proto 存不存在呢?其实是不存在的,打印的话会发现是 null 。这也证明了 Object 是 JavaScript 中数据类型的起源。


分析到这里,我们大概了解原型及构造函数的大概关系了,我们可以用一张图来表示这个关系:

原型链

说完了原型,就可以来说说原型链了,如果理解了原型机制,原型链就很好解释了。其实上面一张图上,那条被 proto 链接起来的链式关系,就称为原型链

原型链的作用:原型链如此的重要的原因就在于它决定了 JavaScript 中继承的实现方式。当我们访问一个属性时,查找机制如下:

  • 访问对象实例属性,有则返回,没有就通过 proto 去它的原型对象查找。
  • 原型对象找到即返回,找不到,继续通过原型对象的 proto 查找。
  • 一层一层一直找到 Object.prototype ,如果找到目标属性即返回,找不到就返回 undefined,不会再往下找,因为在往下找 proto 就是 null 了。

通过上面的解释,对于构造函数生成的实例,我们应该能了解它的原型对象了。JavaScript 中万物皆对象,那么构造函数肯定也是个对象,是对象就有 proto ,那么构造函数的 proto 是什么?

现在才想起来所有的函数可以使用 new Function() 的方式创建,那么这个答案也就很自然了,有点意思,再来试试别的构造函数。

这也证明了,所有函数都是 Function 的实例。等一下,好像有哪里不对,那么 Function.proto 岂不是。。。

按照上面的逻辑,这样说的话,Function 岂不是自己生成了自己?其实,我们大可不必这样理解,因为作为一个 JS 内置对象,Function 对象在你脚本文件都还没生成的时候就已经存在了,哪里能自己调用自己,这个东西就类似于玄学中的“道”和“乾坤”,你能说明它们是谁生成的吗,天地同寿日月同庚不生不灭。。。算了,在往下扯就要写成修仙了=。=
至于为什么 Function.proto 等于 Function.prototype 有这么几种说法:

  • 为了保持与其他函数保持一致
  • 为了说明一种关系,比如证明所有的函数都是 Function 的实例。
  • 函数都是可以调用 call bind 这些内置 API 的,这么写可以很好的保证函数实例能够使用这些 API。

注意点:

关于原型、原型链和构造函数有几点需要注意:

  • proto 是非标准属性,如果要访问一个对象的原型,建议使用 ES6 新增的 Reflect.getPrototypeOf 或者 Object.getPrototypeOf() 方法,而不是直接 obj.proto,因为非标准属性意味着未来可能直接会修改或者移除该属性。同理,当改变一个对象的原型时,最好也使用 ES6 提供的 Reflect.setPrototypeOf 或 Object.setPrototypeOf。
let target = {};
let newProto = {};
Reflect.getPrototypeOf(target) === newProto; // false
Reflect.setPrototypeOf(target, newProto);
Reflect.getPrototypeOf(target) === newProto; // true
  • 函数都会有 prototype ,除了 - Function.prototype.bind() 之外。
    对象都会有 proto ,除了 Object.prototype 之外(其实它也是有的,之不过是 null)。
  • 所有函数都由 Function 创建而来,也就是说他们的 proto 都等于 Function.prototype。
  • Function.prototype 等于 Function.proto

原型污染

  • 原型污染是指:攻击者通过某种手段修改 JavaScript 对象的原型。
  • 什么意思呢,原理其实很简单。如果我们把 Object.prototype.toString 改成这样:
Object.prototype.toString = function () {alert('原型污染')};
let obj = {};
obj.toString();

那么当我们运行这段代码的时候浏览器就会弹出一个 alert,对象原生的 toString 方法被改写了,所有对象当调用 toString 时都会受到影响。
你可能会说,怎么可能有人傻到在源码里写这种代码,这不是搬起石头砸自己的脚么?没错,没人会在源码里这么写,但是攻击者可能会通过表单或者修改请求内容等方式使用原型污染发起攻击,来看下面一种情况:

'use strict';const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');const isObject = obj => obj && obj.constructor && obj.constructor === Object;function merge(a, b) {for (var attr in b) {if (isObject(a[attr]) && isObject(b[attr])) {merge(a[attr], b[attr]);} else {a[attr] = b[attr];}}return a
}function clone(a) {return merge({}, a);
}// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {var body = JSON.parse(JSON.stringify(req.body));var copybody = clone(body)if (copybody.name) {res.cookie('name', copybody.name).json({"done": "cookie set"});} else {res.json({"error": "cookie not set"})}
});
app.get('/getFlag', (req, res) => {var аdmin = JSON.parse(JSON.stringify(req.cookies))if (admin.аdmin == 1) {res.send("hackim19{}");} else {res.send("You are not authorized");}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

如果服务器中有上述的代码片段,攻击者只要将 cookie 设置成{proto: {admin: 1}} 就能完成系统的侵入。


原型污染的解决方案

在看原型污染的解决方案之前,我们可以看下 lodash 团队之前解决原型污染问题的手法:

  1. 代码很简单,只要是碰到有 constructor 或者 proto 这样的敏感词汇,就直接退出执行了。这当然是一种防止原型污染的有效手段,当然我们还有其他手段:
    使用 Object.create(null), 方法创建一个原型为 null 的新对象,这样无论对 原型做怎样的扩展都不会生效:
const obj = Object.create(null);
obj.__proto__ = { hack: '污染原型的属性' };
console.log(obj); // => {}
console.log(obj.hack); // => undefined
  1. 使用 Object.freeze(obj) 冻结指定对象,使之不能被修改属性,成为不可扩展对象:
Object.freeze(Object.prototype);Object.prototype.toString = 'evil';console.log(Object.prototype.toString);
// => ƒ toString() { [native code] }
复制代码
  1. 建立 JSON schema ,在解析用户输入内容时,通过 JSON schema 过滤敏感键名。
  2. 规避不安全的递归性合并。这一点类似 lodash 修复手段,完善了合并操作的安全性,对敏感键名跳过处理。


继承

终于可以来说说继承了,先来看看继承的概念,看下百度上是怎么说的:
继承是面向对象软件技术当中的一个概念,与多态、封装共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。
这段对于程序员来说,这个解释还是比较好理解的。接着往下翻,我看到了一条重要的描述:
子类的创建可以增加新数据、新功能,可以继承父类全部的功能,但是不能选择性的继承父类的部分功能。继承是类与类之间的关系,不是对象与对象之间的关系。
这就尴尬了,JavaScript 里哪里来的类,只有对象。那照这么说岂不是不能实现纯正的继承了?所以才会有开头那句话:与其叫继承,委托的说法反而更准确些。
但是 JavaScript 是非常灵活的, 灵活这一特点给它带来很多缺陷的同时,也缔造出很多惊艳的优点。没有原生提供类的继承不要紧,我们可以用更多元的方式来实现 JavaScript 中的继承,比如说利用 Object.assign:

let person = { name: null, age: null };
let man = Object.assign({}, person, { name: 'John', age: 23 });
console.log(man);  // => { name: 'John', age: 23 }
复制代码
利用  call 和 apply:let person = {name: null,sayName: function () {console.log(this.name);},sayAge: function () {console.log(this.age);}
};
let man = { name: 'Man', age: 23 };
person.sayName.call(man); // => Man
person.sayAge.apply(man); // => 23
复制代码

甚至我们还可以使用深拷贝对象的方式来完成类似继承的操作……JS 中实现继承的手法多种多样,但是看看上面的代码不难发现一些问题:

  • 封装性不强,过于凌乱,写起来十分不便。
  • 根本无法判断子对象是从何处继承而来。
    有没有办法解决这些问题呢?我们可以使用 JavaScript 中继承最常用的方式:原型继承

原型链继承

原型链继承,就是让对象实例通过原型链的方式串联起来,当访问目标对象的某一属性时,能顺着原型链进行查找,从而达到类似继承的效果。

// 父类
function SuperType (colors = ['red', 'blue', 'green']) {this.colors = colors;
}// 子类
function SubType () {}
// 继承父类
SubType.prototype = new SuperType();
// 以这种方式将 constructor 属性指回 SubType 会改变 constructor 为可遍历属性
SubType.prototype.constructor = SubType;let superInstance1 = new SuperType(['yellow', 'pink']);
let subInstance1 = new SubType();
let subInstance2 = new SubType();
superInstance1.colors; // => ['yellow', 'pink']
subInstance1.colors; // => ['red', 'blue', 'green']
subInstance2.colors; // => ['red', 'blue', 'green']
subInstance1.colors.push('black');
subInstance1.colors; // => ['red', 'blue', 'green', 'black']
subInstance2.colors; // => ['red', 'blue', 'green', 'black']

上述代码使用了最基本的原型链继承使得子类能够继承父类的属性,原型继承的关键步骤就在于:将子类原型和父类原型关联起来,使原型链能够衔接上,这边是直接将子类原型指向了父类实例来完成关联。
上述是原型继承的一种最初始的状态,我们分析上面代码,会发现还是会有问题:

  • 在创建子类实例的时候,不能向超类型的构造函数中传递参数。
  • 这样创建的子类原型会包含父类的实例属性,造成引用类型属性同步修改的问题。

组合继承
组合继承使用 call 在子类构造函数中调用父类构造函数,解决了上述两个问题:


// 组合继承实现function Parent(value) {this.value = value;
}Parent.prototype.getValue = function() {console.log(this.value);
}function Child(value) {Parent.call(this, value)
}Child.prototype = new Parent();const child = new Child(1)
child.getValue();
child instanceof Parent;

然而它还是存在问题:父类的构造函数被调用了两次(创建子类原型时调用了一次,创建子类实例时又调用了一次),导致子类原型上会存在父类实例属性,浪费内存。

寄生组合继承

针对组合继承存在的缺陷,又进化出了“寄生组合继承”:使用 Object.create(Parent.prototype) 创建一个新的原型对象赋予子类从而解决组合继承的缺陷:


// 寄生组合继承实现function Parent(value) {this.value = value;
}Parent.prototype.getValue = function() {console.log(this.value);
}function Child(value) {Parent.call(this, value)
}Child.prototype = Object.create(Parent.prototype, {constructor: {value: Child,enumerable: false, // 不可枚举该属性writable: true, // 可改写该属性configurable: true // 可用 delete 删除该属性}
})const child = new Child(1)
child.getValue();
child instanceof Parent;
复制代码

寄生组合继承的模式是现在业内公认的比较可靠的 JS 继承模式,ES6 的 class 继承在 babel 转义后,底层也是使用的寄生组合继承的方式实现的。

继承关系判断

当我们使用了原型链继承后,怎样判断对象实例和目标类型之间的关系呢?
instanceof
我们可以使用 instanceof 来判断二者间是否有继承关系,instanceof 的字面意思就是:xx 是否为 xxx 的实例。如果是则返回 true 否则返回 false:

function Parent () {}
function Child () {}
Child.prototype = new Parent();
let parent = new Parent();
let child = new Child();parent instanceof Parent; // => true
child instanceof Child; // => true
child instanceof Parent; // => true
child instanceof Object; // => true

instanceof 本质上是通过原型链查找来判断继承关系的,因此只能用来判断引用类型,对基本类型无效,我们可以手动实现一个简易版 instanceof:

function _instanceof (obj, Constructor) {if (typeof obj !== 'object' || obj == null) return false;let construProto = Constructor.prototype;let objProto = obj.__proto__;while (objProto != null) {if (objProto === construProto) return true;objProto = objProto.__proto__;}return false;
}

Object.prototype.isPrototypeOf(obj)
还可以利用 Object.prototype.isPrototypeOf 来间接判断继承关系,该方法用于判断一个对象是否存在于另一个对象的原型链上:

function Foo() {}
function Bar() {}
function Baz() {}Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);var baz = new Baz();console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true

JavaScript 原型链和继承面试题相关推荐

  1. JavaScript原型链实现继承

    js 继承 原型链 默认的原型 确定原型和实例的关系 谨慎定义方法 原型链的问题 借用构造函数 组合继承 最常用的继承模式 原型式继承 寄生式继承 寄生组合式继承 是引用类型最理想的继承范式 学习记录 ...

  2. 对Javascript 类、原型链、继承的理解

    一.序言   和其他面向对象的语言(如Java)不同,Javascript语言对类的实现和继承的实现没有标准的定义,而是将这些交给了程序员,让程序员更加灵活地(当然刚开始也更加头疼)去定义类,实现继承 ...

  3. 图解JavaScript原型链继承

    JavaScript是基于原型链的继承的,忘掉类的继承,从原型链入手. 普通对象 函数对象 JavaScrip只有一种结构:对象 通过new Function()创建的对象都是函数对象,其他都是普通对 ...

  4. JavaScript (四) ——构造函数原型 , 原型链 和继承

    原型 所有引用类型都有一个_proto_属性, 属性值是对象 所有函数都有一个prototype属性 , 属性值是一个对象 所有引用类型的_proto_属性 , 都指向其构造函数的prototype ...

  5. 技术分享经典 javaScript原型链面试题

    技术分享 javaScript原型链 一个小题目 前置知识 变量提升和函数提升 this指针的指向 原型链是什么 new操作符的工资流程 一个小题目 今天我们部门的技术分享上出现了这样一段代码: fu ...

  6. JS基于原型链的继承和基于类的继承学习总结

    1. new关键字创建一个对象的过程 (1) 创建一个空的简单对象(即{}) (2)为步骤1新创建的对象添加属性_proto_,该属性连接至构造函数的原型对象 (3)将步骤1新创建的对象作为this的 ...

  7. javascript原型链中 this 的指向

    为了弄清楚Javascript原型链中的this指向问题,我写了个代码来测试: var d = {d: 40};var a = {x: 10,calculate: function (z) {retu ...

  8. JavaScript 原型链总结(一)

    JS原型链(一) 文章目录 JS原型链(一) 一.对象构造函数,原型对象,实例对象之间的关系 二.原型链继承 三.常用API 四.常见问题 一.什么是原型与原型链 二.原型和原型链存在的意义是什么? ...

  9. 深度解析JavaScript原型链

    深度解析JavaScript原型链 文章目录 深度解析JavaScript原型链 前言 JavaScript原型链,这里只分享我自己的见解 一.原型链是什么 二.心得 三图解 总结 前言 JavaSc ...

最新文章

  1. 3.Utm详细实现-用户生命流程
  2. Cisco easy *** basic ASA
  3. Leetcode 217. 存在重复元素 (每日一题 20210913)
  4. SQL Server查询中特殊字符的处理方法
  5. 炁体源流 鸿蒙,一人之下:八绝技中最强被曝光,没想到炁体源流落榜,第一在后头...
  6. Python使用多线程搜索指定范围内的所有素数
  7. 数据分析师工资高达50万,正在进入每一个行业!
  8. requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
  9. storm throw 口袋妖怪_pokemon go游戏术语都有哪些 口袋妖怪go玩法术语攻略
  10. matlab 相关性分析 相关系数地图生成
  11. 全文标明引文报告html,知网查重全文标明引文是什么意思
  12. tp-link tl-wr740n 虚拟服务器,TP-Link TL-WR740N无线路由器的上网设置教程
  13. DAO终极之问:去中心化组织归谁所有?
  14. xftp,xftp和ftp
  15. Django Ninja简单教程
  16. LM1640数码管驱动芯片的使用方法
  17. AI+RPA技术在反洗钱中的应用
  18. MC6C迈克/FLYSKY富斯/WFLY2天地飞二代接收机远程刷固件教程
  19. 使用python登陆Yahoo邮箱
  20. 基于JAVAWeb产品管理系统计算机毕业设计源码+数据库+lw文档+系统+部署

热门文章

  1. 高斯消元法求解方程组(要有python基础和线性代数的基础)
  2. 富翁与陌生人换钱游戏
  3. 你真的了解USB吗?USB充电大揭秘(一)
  4. 安装Office2016,桌面没有PPT图标,右键新建没有图标
  5. js检测浏览器类型以及版本信息
  6. Spring Batch之读数据—FlatFileItemReader(二十五)
  7. 解放束缚畅享优美洪亮音色 户外广场舞专用拉杆音箱就选它
  8. Coding for NEON - Part 3: Matrix Multiplication
  9. 苹果6手机怎么录屏_OPPO手机怎么录屏
  10. 对口升学班计算机英语,对口升学计算机专业试题及对口升学英语模拟试卷.doc...