JavaScript
中没有类的概念的,主要经过原型链来实现继承。一般状况下,继承意味着复制操做,然而 JavaScript
默认并不会复制对象的属性,相反,JavaScript
只是在两个对象之间建立一个关联(原型对象指针),这样,一个对象就能够经过委托访问另外一个对象的属性和函数,因此与其叫继承,委托的说法反而更准确些。javascript
当咱们
new
了一个新的对象实例,明明什么都没有作,就直接能够访问toString
、valueOf
等原生方法。那么这些方法是从哪里来的呢?答案就是原型。前端
在控制台打印一个空对象时,咱们能够看到,有不少方法,已经“初始化”挂载在内置的 __proto__
对象上了。这个内置的 __proto__
是一个指向原型对象的指针,它会在建立一个新的引用类型对象时(显示或者隐式)自动建立,并挂载到新实例上。当咱们尝试访问实例对象上的某一属性 / 方法时,若是实例对象上有该属性 / 方法时,就返回实例属性 / 方法,若是没有,就去 __proto__
指向的原型对象上查找对应的属性 / 方法。这就是为何咱们尝试访问空对象的 toString
和 valueOf
等方法依旧能访问到的缘由,JavaScript
正式以这种方式为基础来实现继承的。java
若是说实例的 __proto__
只是一个指向原型对象的指针,那就说明在此以前原型对象就已经建立了,那么原型对象是何时被建立的呢?这就要引入构造函数的概念。webpack
其实构造函数也就只是一个普通的函数而已,若是这个函数可使用 new
关键字来建立它的实例对象,那么咱们就把这种函数称为 构造函数。git
// 普通函数
function person () {}
// 构造函数,函数首字母一般大写
function Person () {}
const person = new Person();
复制代码
原型对象正是在构造函数被声明时一同建立的。构造函数被申明时,原型对象也一同完成建立,而后挂载到构造函数的 prototype
属性上:程序员
原型对象被建立时,会自动生成一个 constructor
属性,指向建立它的构造函数。这样它俩的关系就被紧密地关联起来了。github
细心的话,你可能会发现,原型对象也有本身的
__proto__
,这也不奇怪,毕竟万物皆对象嘛。原型对象的 __proto__ 指向的是Object.prototype
。那么Object.prototype.__proto__
存不存在呢?实际上是不存在的,打印的话会发现是null
。这也证实了Object
是JavaScript
中数据类型的起源。web
分析到这里,咱们大概了解原型及构造函数的大概关系了,咱们能够用一张图来表示这个关系:面试
说完了原型,就能够来讲说原型链了,若是理解了原型机制,原型链就很好解释了。其实上面一张图上,那条被 __proto__
连接起来的链式关系,就称为原型链。express
原型链的做用:原型链如此的重要的缘由就在于它决定了 JavaScript
中继承的实现方式。当咱们访问一个属性时,查找机制以下:
__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
)。__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
团队以前解决原型污染问题的手法:
代码很简单,只要是碰到有 constructor
或者 __proto__
这样的敏感词汇,就直接退出执行了。这固然是一种防止原型污染的有效手段,固然咱们还有其余手段:
Object.create(null)
, 方法建立一个原型为 null
的新对象,这样不管对 原型作怎样的扩展都不会生效:const obj = Object.create(null);
obj.__proto__ = { hack: '污染原型的属性' };
console.log(obj); // => {}
console.log(obj.hack); // => undefined
复制代码
使用 Object.freeze(obj)
冻结指定对象,使之不能被修改属性,成为不可扩展对象:
Object.freeze(Object.prototype);
Object.prototype.toString = 'evil';
console.log(Object.prototype.toString);
// => ƒ toString() { [native code] }
复制代码
创建 JSON schema
,在解析用户输入内容时,经过 JSON schema
过滤敏感键名。
规避不安全的递归性合并。这一点相似 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
的字面意思就是: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
来间接判断继承关系,该方法用于判断一个对象是否存在于另外一个对象的原型链上:
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
复制代码
本篇文章已收录入 前端面试指南专栏