JavaScript 原型的使用
原型的英文名是:prototype
每个JS对象都有原型(即内部的[[Prototype]]
属性)。
无法通过key的方式直接访问这个属性,也无法在控制台显示这个属性。
访问 [[Prototype]] 的方式
访问对象原型的方式有3个:
- (浏览器提供的)非标准属性
__propto__
var proto = {age: 18}
var person = Object.create(proto)
console.log(person.__proto__ === proto) // true
__proto__
是浏览器提供的非标准属性,意味着未来会修改或移除该属性。
Object.getPrototypeOf()
- 返回指定对象的原型(内部[[Prototype]]
属性的值)
var proto = {age: 18}
var person = Object.create(proto)
console.log(Object.getPrototypeOf(person) === proto) // true
Reflect.getPrototypeOf()
- 类似于Object.getPrototypeOf()
Reflect 是一个内置的对象,提供了一些与Object
的方法相同,但存在细微差别的静态方法。
建议使用标准语法:Object.getPrototypeOf()
或 Reflect.getPrototypeOf()
设置 [[Prototype]] 的方式
- 可以通过对
__proto__
属性直接赋值的方式修改对象的原型
var proto = {age: 18}
var person = Object.create(proto)
person.__proto__ = {name:'Jack'}
console.log(Object.getPrototypeOf(person)) // {name: "Jack"}
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()
创建的实例对象的原型。
而这个实例对象的原型,指向构造它的构造器Object
的prototype
属性。
所以再继续查询,就是在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__
。