JavaScript 原型和原型链(__proto__和prototype)

JavaScript 原型的使用

原型的英文名是:prototype

每个JS对象都有原型(即内部的[[Prototype]]属性)。

无法通过key的方式直接访问这个属性,也无法在控制台显示这个属性。

访问 [[Prototype]] 的方式

访问对象原型的方式有3个:

  1. (浏览器提供的)非标准属性__propto__
var proto = {age: 18}
var person = Object.create(proto)
console.log(person.__proto__ === proto) // true

__proto__是浏览器提供的非标准属性,意味着未来会修改或移除该属性。

  1. Object.getPrototypeOf() - 返回指定对象的原型(内部[[Prototype]]属性的值)
var proto = {age: 18}
var person = Object.create(proto)
console.log(Object.getPrototypeOf(person) === proto) // true
  1. Reflect.getPrototypeOf() - 类似于Object.getPrototypeOf()

Reflect 是一个内置的对象,提供了一些与Object的方法相同,但存在细微差别的静态方法。

建议使用标准语法:Object.getPrototypeOf()Reflect.getPrototypeOf()

设置 [[Prototype]] 的方式

  1. 可以通过对__proto__属性直接赋值的方式修改对象的原型
var proto = {age: 18}
var person = Object.create(proto)
person.__proto__ = {name:'Jack'}
console.log(Object.getPrototypeOf(person)) // {name: "Jack"}
  1. Object.setProptotypeOf()Reflect.setProptotypeOf()
var proto = {age: 18}
var person = Object.create(proto)
Object.setPrototypeOf(person, {name:'Jack'})
// Reflect.setPrototypeOf(person, {name:'Jack'})
console.log(Object.getPrototypeOf(person)) // {name: "Jack"}
  • 建议使用 setProptotypeOf()
  • 被设置的值只能是对象 或 null,其他类型的值:
    • __proto__方式不生效
    • setProptotypeOf()方式及会报错
  • 如果对象不可扩展,设置对象的原型会抛出TypeError
    • 例如被冻结(Object.freeze())的对象
    • 可通过Object.isExtensible(obj)判断是否可扩展

原型和构造器

Object.create(proto) 以传入的对象(proto)为原型创建一个对象(obj)。

obj 的原型[[Prototype]] 就指向这个对象 proto

var proto = { age: 18 }
var obj = Object.create(proto)
console.log(Object.getPrototypeOf(obj) === proto) // true

构造器(constructor) 是一个通过 new 操作符调用的普通函数,也叫构造函数

  • 它的作用是创建一个对象实例(也是一个对象)
  • 内部定义实例的成员/属性(this.age=18
  • 返回值应该是以下几种情况:
    • 没有return
    • return this
    • return 基本数据类型
  • 如果通过 new 调用的函数,内部 return 一个对象,那就会把这个对象返回给外界,这样就是一个普通的函数调用,而不是构造函数
    • 原型的指向也会与构造器构造的实例对象不同
  • 构造函数是一个普通函数,因为内部要使用 this
    • 普通函数内部this指向调用者 或 创建的实例
    • 箭头函数内部this指向当前定义函数的上下文,所以不能作为构造函数
// 构造器一般使用大驼峰命名
var Person = function P() {
  this.age = 18
}
// 创建一个Person的实例
var tom = new Person()

每个构造器都有一个 prototype 属性,默认是一个空对象(没有可遍历的属性),它包含的属性:

  • constructor - 指向构造器本身(f P()
  • [[Prototype]] - 指向实例化自己的构造器的原型
    • 因为函数也是一个由构造器创建的对象
    • 函数的构造器,是 Function
    • 它指向构造函数 Function 的原型([[Prototype]]
  • 其他额外定义在原型上的属成员

由构造器创建的实例对象的原型都指向构造器的 prototype 属性。

上例就是:

console.log(tom.__proto__ === Person.prototype) // true
console.log(Object.getPrototypeOf(Person) === Function.prototype) // true

[[Prototype]] 和 prototype

[[Prototype]] 无法直接访问,所以很多人都用浏览器的非标准属性__proto__来表示。

__proto__prototype 很容易搞混。

从翻译结果看,都可以叫原型,但是是完全不同的两个东西。

一般说的原型指的是[[Prototype]]

  • [[Prototype]] 存在于所有的对象上
  • prototype 存在于所有的函数上

二者的关系是:函数的prototype是所有使用 new 这个函构造的实例的 [[Prototype]]

注意:如果函数不是作为构造器使用(参考上面构造器的返回值介绍),new 构造的就不是这个函数的实例,而是函数返回对象,上面的关系就不成立。

var Person = function() {
  return { age: 18 }
}
var person = new Person()
console.log(Object.getPrototypeOf(person) == Person.prototype) // false

函数也是用 构造器创建的对象 new Function,所以函数同时有 [[Prototype]]prototype

原型的特点 - 原型链

当在一个对象obj上访问某个属性式,如果obj自身不存在这个属性,就会去obj的原型[[Prototype]]上寻找,即 obj.__proto__

有则返回,无则继续向下寻找,也就是去obj.__proto__的原型obj.__proto__.__proto__上寻找。

寻找的终点指向 Object.prototype.__proto__,值是null,此时属性就会返回 undefined

各个原型之间构成的链,称之为原型链。

访问对象属性按照原型链递归查询,直到 undefined。

原型链终点

同函数一样,所有对象最初都是由 Object 构造器创建的实例对象 new Object()

每个对象按照原型链查询,最终都会查到 new Object() 创建的实例对象的原型。

而这个实例对象的原型,指向构造它的构造器Objectprototype属性。

所以再继续查询,就是在Object.prototype.__proto__上查。

它的值是null,所以就不会继续查询了,最终返回undefined

// Object.create() 以传入的对象为原型创建一个对象
var obj1 = { foo: 'bar' }
// 校验 obj1 的原型是否指向 构造器 Object的protoytype
console.log(Object.getPrototypeOf(obj1) === Object.prototype) // true
// 校验 obj1 是否是 构造器 Object 创建的实例
console.log(obj1 instanceof Object) // true

var obj2 = Object.create(obj1)
var obj3 = Object.create(obj2)

console.log(obj3.foo) // bar
console.log(obj3.paz) // undefined

// paz 查询过程:
// 1. 查询obj3中的paz属性,不存在该属性,在obj3的原型上查询 ==> obj3.__proto__ = obj2
// 2. 查询obj2中的paz属性,不存在该属性,在obj2的原型上查询 ==> obj2.__proto__ = obj1
// 3. 查询obj1中的paz属性,不存在该属性,在obj1的原型上查询 ==> obj1.__proto__ = Object.prototype
// 4. 查询Object.prototype中的paz属性,不存在该属性
//    在Object.prototype的原型上查询 ==> Object.prototype.__proto__ = null
// obj3.paz 返回 undefined

在这里插入图片描述

原型的用途

结合原型的特点和构造器的使用:

  • 访问对象属性会沿着原型链寻找
  • 构造器创建的实例对象的原型指向构造器的prototype(不可遍历属性)

可以通过构造器创建一个实例对象,然后扩展构造器函数的prototype属性上的成员,例如添加属性或方法。

以使它创建的实例对象,都可以访问这些成员,并且这些成员只需要占用一份内存。

例如:Object.prototype 上的 toString 方法,所有对象都可以调用。

原型污染

原型污染指的是:攻击者通过某种手段修改 JavaScript 对象的原型。

虽然说任何一个原型被污染了都有可能导致问题,但是我们一般提原型污染说的就是 Object.prototype 被污染。

原型上的属性可以通过遍历访问到的,所以原型污染会引起性能消耗意外BUG

例如:

Object.prototype.hack = '污染原型的属性'
const obj = { age:18 }
for (const key in obj) {
  console.log(`${key}: ${obj[key]}`)
}
/*
age: 18
hack: 污染原型的属性
*/

虽然可以通过 hasOwnProperty() 方法或Object.keys() 区分属性是否是对象自身的还是原型的,但这还是增加了额外的遍历。

预防原型污染

开发者常用的预防原型污染的方法。

  • Object.create(null) 创建没有原型的对象
    • 原型不指向 Object.prototype,也就不会受 Object.prototype 影响
  • Object.freeze(obj) 冻结对象
    • 禁止增删改对象上的属性,防止原型属性[[Prototype]]的修改

这两个方法从两个方向(源头、自身)预防原型污染。

原型污染大多发生在调用会修改或者扩展对象属性的函数时,例如 lodash 的 defaults,jquery 的 extend。

预防原型污染最主要还是要有防患意识,养成良好的编码习惯。

__proto__

__proto__是浏览器提供的可以访问对象原型的属性。

  • Object.prototype上,它其实就是浏览器使用 getter/setter 属性描述符定义的访问器属性。

    • __porto__getter内获取对象的[[Prototype]]
  • 在其他对象上,它就指向对象的原型[[Prototype]]

var obj1 = { age: 18 }
var obj2 = Object.create(obj1)

console.log(obj2)
console.log(Object.getOwnPropertyDescriptor(Object.prototype,'__proto__'))

在这里插入图片描述

所以下面这种方式修改原型会有一些差异:

var obj = { age: 18 }
obj.__proto__ = null
// 结果是 __proto__ 属性被删除了
console.log(obj.__proto__) // undefined
// 但是[[Portotype]]设置成功了
console.log(Object.getPrototypeOf(obj)) // null
// 再次设置,此时__proto__作为一个普通属性被赋值
obj.__proto__ = null
// _proto__ 设置成功
console.log(obj.__proto__) // null
// 原型无影响
console.log(Object.getPrototypeOf(obj)) // null

// 测试结果,与原型无关
obj.__proto__ = {name:'Jack'}
console.log(obj.name) // undefined

所以不建议使用__proto__

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值