美文网首页
原型链和继承

原型链和继承

作者: __Nancy | 来源:发表于2020-05-09 19:55 被阅读0次

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

原型

当我们 new 了一个新的对象实例,明明什么都没有做,就可以直接访问toStringvalueOf等原生方法。那么这些方法是从什么地方来的呢?答案就是原型

image.png

在控制台打印一个空对象的时候,我们可以看到,有很多方法,已经“初始化”挂载在内置的__proto__对象上了。这个内置的__proto__是一个指向原型对象的指针,它会创建一个新的引用类型对象时(显示或者隐藏)自动创建,并挂载到新的实例上。当我们尝试访问实例对象上的某一属性/方法时,如果实例对象上有该属性/方法时,就会返回实例属性/方法,如果没有,就去__proto__指向的原型对象上查找对应的属性/方法。这就是问什么我们尝试访问空对象的toStringvulueOf等方法依旧能访问到的原因,js正是以这种方式为基础来实现继承的。

构造函数

如果说实例的__proto__只是一个指向原型对象的指针,那就说明在此之前原型对象就已经创建来,那么原型对象是什么时候被创建的呢?这就要引入构造函数的概念

// 普通函数
function person () {}

// 构造函数,函数首字母通常大写
function Person () {}
const person = new Person();

原型对象正是在构造函数被声明时一同被创建的,构造函数被申明时,原型对象也一同完成创建,然后挂载到构造函数的prototype属性上

image.png

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

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

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

image.png

原型链

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

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

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

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

image.png

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

image.png

这也证明了,所有的函数都是Function的实例。那么Function.__proto__,那么Function.__proto__ 岂不是。。。

按照上面的逻辑,这样说的话,function 岂不是自己生成了自己?其实,我们大可不必这样理解,因为作为一个js``内置对象,function``` 对象在你的脚本文件都还没有生成的时候就已经存在了,哪里能自己调用自己。

至于为什么Function.__proto__ 等于 Function.prototype有这么几种说法:

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

注意点

关于原型、原型链和构造函数有几点需要注意:
-__proto__是非标准属性,如果要访问一个对象的原型,建议使用ES6新增的Reflect.getPrototypeOf或者 Object.getPrototypeOf()方法。同理,当改变一个对象的原型时,最好也使用ES6提供的Reflect.setPrototypeOfObject.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 团队之前解决原型污染问题的手法:

image.png

代码很简单,只要是碰到有constructor或者__proto__这样的敏感词汇,就直接退出执行了。这当然是一种防止原型污染的有效手段,当然我们还有其他手段:

  1. 使用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修复手段,完善了合并操作的安全性,对敏感键名跳过处理。

继承

概念:

继承是面向对象软件技术当中的一个概念,与多态、封装共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。

这段对于程序员来说,这个解释还是比较好理解的。接着:

子类的创建可以增加数据、新功能,可以继承父类的全部功能,但是不能选择性的继承父类的部分功能。继承是类与类之间的关系,不是对象与对象之间的关系。

这就尴尬了。js里哪来的类,只有对象。哪照这么说岂不是不能实现纯正的继承了?所以才会有开头那句:与其叫继承,委托的说话反而更准确些

但是js是非常灵活的,灵活这一特点给它带来很多缺陷的同时,也缔造出很多惊艳的有点。没有原生提供类的继承不要紧,我们可以用更多元的方式来实现js中的继承,比如说利用Object.assign

let person = { name: null, age: null };
let man = Object.assign({}, person, { name: 'John', age: 23 });
console.log(man);  // => { name: 'John', age: 23 }

利用 callapply

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 中实现继承的手法多种多样,但是看看上面的代码不难发现一些问题:

  • 分装性不强,过于凌乱,写起来十分不便。
  • 根本无法判断子对象是何处继承来的。

有没有办法解决这些问题呢?我们可以使用js中继承最常用的方式:原型继承

原型链继承

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

// 父类
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']

上述代码使用了最基本的原型链继承使的子类能够继承父类的属性,原型继承的关键步骤就在于:将子类原行和父类原型关联起来,使原型链能够衔接上,这边是直接将子类原型指向了父类实例来完成关联。

上述是原型继承的一种最初始的状态。我们分析上面的代码,会发现还是会有问题:

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

组合继承

组合继承使用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

转自:https://juejin.im/post/5eb52ad9e51d454de64e4306
如果帮组到您,请举小手手赞一下,笔芯 ❤❤❤

相关文章

  • JavaScript 原型、原型链与原型继承

    原型,原型链与原型继承 用自己的方式理解原型,原型链和原型继承 javascript——原型与原型链 JavaSc...

  • JavaScript继承方式详解

    JavaScript实现继承的方式主要有两种: 原型链继承和借助构造函数继承 一、原型链继承 原型链继承的主要思想...

  • js实现继承的几种方式

    js实现继承有几种方式,这里我们主要探讨 原型链继承 构造继承 组合继承(原型链和构造继承组合到一块,使用原型链实...

  • 继承

    原型链直接继承 原型链直接继承prototype 原型链继承_prototype属性 继承_构造函数绑定

  • js中的实现继承的几种方式

    大纲:原型链借用构造函数组合继承原型式继承寄生式继承寄生组合式继承 1、原型链: 什么是原型链? 原型链的基本思想...

  • 第六章(3):继承

    继承的几种方式 原型链 原型链示意图: 构造函数 组合继承(将原型链和构造函数组合在一起) 原型式继承 寄生式组合...

  • js基础之实现继承的几种方式

    js 实现继承的方式有: 原型链继承; 构造函数继承; 组合继承(原型链继承 + 构造函数继承)(最常用);(原型...

  • es5的部分继承以及es6的class

    一、JavaScript常用的原型继承方式 原型链继承 2,构造函数继承(对象冒充继承) 3,组合继承(原型链继承...

  • 原型与继承

    什么是继承? 继承父级的属性和方法和共享(原型链)的属性和方法 组合继承 通过原型链继承共享的方法和属性;通过构造...

  • 构造函数原型的继承方式分析

    1.通过原型链继承 综上我们可以总结出 通过原型链来实现继承的原理通过原型链来实现继承的原理原型链继承方案中,父类...

网友评论

      本文标题:原型链和继承

      本文链接:https://www.haomeiwen.com/subject/jpslnhtx.html