原型链
每一个函数拥有原型(prototype)属性,如果该函数充当构造函数去 new 实例,则会构成一条原型链,每个实例成员都可以使用原型上的属性和方法
function Person() {}
Person.prototype.say = function() {
console.log('hello')
}
const jack = new Person()
const marry = new Person()
jack.say() // hello
marry.say() // hello
复制代码
一个构造函数与它的原型及实例的关系图如下:
function Person() {}
const jack = new Person()
console.log(jack.__proto__ === Person.prototype) // true,实例对象-→原型
console.log(jack.constructor === Person) // true,实例对象-→构造函数
console.log(Person.prototype.constructor === Person) // true,构造函数←→原型
复制代码
任何原型链的最顶层都指向
Object
的原型,只需要记住原型、构造函数、实例对象三者的关系即可,把其他原型当成 Object 原型的实例对象
function Person() {}
const jack = new Person()
console.log(jack.__proto__.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
复制代码
特性
原型
原型本质是一个对象,其内部包含默认属性值 constructor
和 __proto__
,在原型中添加的属性会被添加到该对象中,且原型会作为实例对象的父层作用域对象,当实例对象找不到指定属性时,会往上从自身原型中查找
构造函数的静态属性与原型及实例对象没有作用域的关联,因此静态属性不会影响原型及实例对象
function Person() {}
const jack = new Person()
Person.prototype.x = 'x'
Person.y = 'y'
console.log(jack.x) // x
console.log(jack.y) // undefined
console.log(Person.x) // undefined
console.log(Person.y) // y
复制代码
上述代码执行后的原型内部属性:
注意,父级作用域的概念不只存在于
实例对象 -- 原型
之间,也存在于原型 -- 原型
之间
function Person() {}
const jack = new Person()
Object.prototype.x = 'x'
console.log(jack.x) // x
console.log(jack.__proto__.x) // x,Person原型中没有x属性,会从上级原型中进行查找
复制代码
风险
实例对象更改原型属性
当实例对象想要读取属性数据时,发生如下操作:
- 判断实例中有无该指定属性,如果找到,则直接读取
- 如果没有,则往原型中寻找,直至找到顶层作用域,没有返回 undefined
当实例对象想要写入数据属性时,发生如下操作:
- 判断实例中有无该指定属性,如果找到,则直接修改
- 默认无法对原型数据进行修改,因此在实例对象中创建该属性再进行写入数据
function Person() {}
const jack = new Person()
Person.prototype.x = 'x'
Object.prototype.y = 'y'
console.log(jack.x) // x,从自身原型中读取
console.log(jack.y) // y,自身原型找不到,继续往上级Object原型中读取
console.log(jack.z) // undefined,都找不到则返回undefined
jack.x = 'jack_x'
console.log(jack.__proto__.x) // x,实例对象无法修改原型属性
console.log(jack.x) // jack_x
console.log(jack) // Person {x: "jack_x"},在自身内部添加了x属性
复制代码
不过写操作的限制只是一层浅保护,它的判断是值类型和引用类型的引用地址是否前后相等,如果实例对象直接更改原型引用类型属性中的内部属性,是可以进行修改的(和 const 允许修改引用类型内部属性一样)
function Person() {}
const jack = new Person()
const lucy = new Person()
Person.prototype.obj = {
x: 'x'
}
jack.obj.x = 'jack_x'
console.log(jack.__proto__.obj.x) // jack_x,原型内部属性被实例对象修改了
console.log(jack) // Person {},实例对象内部并不会增加属性
console.log(lucy.obj.x) // jack_x,由于原型属性被修改,所以所有的实例对象都会被影响
复制代码
为了不让实例对象有机会更改原型属性,可以在实例内部增加同名属性,由于优先寻找自身作用域,就可以避免上述情况
由于这个特性,实例对象需要写操作的属性,一般都是自身拥有而非存储在原型上,所以原型一般存储值类型和函数
function Person() {}
const jack = new Person()
const lucy = new Person()
Person.prototype.obj = {
x: 'x'
}
jack.obj = {x: 'jack_x'}
console.log(jack.__proto__.obj.x) // x
console.log(jack) // Person { obj: { x: 'jack_x' } }
console.log(lucy.obj.x) // x
复制代码
原型方法丢失this
在原型中绑定函数属性时,如果声明函数使用的是箭头函数的方式,则会丢失 this 值,因为箭头函数不绑定 this
function Person() {}
const jack = new Person()
Person.prototype.say = () => {
console.log(`my name is ${this.name}`)
}
Person.prototype.doing = function() {
console.log(`i am doing ${this.thing}`)
}
jack.name = 'jack'
jack.thing = 'cooking'
jack.say() // my name is undefined
jack.doing() // i am doing cooking
复制代码
构造函数
任何函数(箭头函数除外)只要通过 new
来生成实例化对象就可以作为构造函数,否则普通函数并无区别,为了与普通函数的功能区分,一般用作构造函数的函数名都会首字母大写
构造函数生成实例化对象本质上是建立原型链之间的关联
new
执行机制
使用new操作符创建对象实例时发生的事情:
- 在构造函数中自动创建一个空对象充当实例对象
- 建立原型链,空对象指向构造函数的原型对象
- 将构造函数的作用域赋给新对象(this指向该对象)并执行构造函数的代码(因此构造函数中使用this声明的属性和方法会复制给新对象)
- 如果是返回值为原始类型,则返回变更为 return this,如果返回值为对象,则正常返回对象
function Person(name) {
this.name = name
}
// const jack = new Person()相当于:
const jack = {}
jack.name = 'name' // 因为有this的存在,new的时候this指向jack,执行构造函数等同于该行代码
jack.__proto = Person.prototype
jack.constructor = Person
复制代码
构造函数公开的属性和方法需要使用 this 表示,否则不会在实例化对象的时候在该对象中创建对应值
function Person() {
this.name = 'jack'
const age = 12
}
const jack = new Person()
console.log(jack) // Person { name: 'jack' }
复制代码
new 将 this 绑定给实例对象的优先级高于更改 this 函数
const obj = {}
function Person() {
this.name = 'Person'
}
const _Person = Person.bind(obj)
const jack = new _Person()
console.log(jack) // Person { name: 'Person' },this仍是指向jack,在内部创建了name属性
复制代码
内部实现
function _new(Fn, ...rest) {
const instance = Object.create(Fn.prototype) // 创建空对象{},并连接实例对象到原型的链路
const result = Fn.apply(instance, rest) // 将this指向实例,传入构造参数执行构造函数,并接收构造函数返回值
return result instanceof Object ? result : instance // 如果构造函数返回值为引用类型则直接返回,否则返回实例对象
}
复制代码
测试原型链连接
function Person(name) {
this.name = name
this.say = () => {
console.log(`my name is ${this.name}`)
}
}
Person.prototype.x = 'x'
const jack = _new(Person, 'jack')
jack.say() // my name is jack
console.log(jack.x) // x
console.log(jack.constructor === Person) // true
复制代码
测试构造函数有返回值情况
function Person() {
return new Map()
}
function Animal() {
return 'animal'
}
const jack = _new(Person)
const lulu = _new(Animal)
console.log(jack) // Map(0) {}
console.log(lulu) // Animal {}
复制代码
箭头函数
箭头函数使用 new
关键字会报错,因此箭头函数无法作为构造函数使用,其本质原因:
- 箭头函数拥有
__proto__
属性,其自身存在原型链,但是没有prototype
属性,导致无法连接原型链 - 箭头函数没有 this 所以无法将构造函数的公开属性传递给实例对象
const fn = () => {}
Function.prototype.x = 'x'
const instance = new fn() // 报错:fn is not a constructor
console.log(fn.__proto__.x) // x,说明箭头函数与Funtion原型链有所关联
console.log(fn.prototype) // undefined,箭头函数没有原型属性
复制代码
构造函数继承
两个构造函数之间可以实现继承关系,如所有构造函数都继承于 Object
构造函数
console.log(Number.prototype.__proto__ === Object.prototype) // true
console.log(Boolean.prototype.__proto__ === Object.prototype) // true
console.log(String.prototype.__proto__ === Object.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Array.prototype.__proto__ === Object.prototype) // true
console.log(Map.prototype.__proto__ === Object.prototype) // true
console.log(Set.prototype.__proto__ === Object.prototype) // true
console.log(Date.prototype.__proto__ === Object.prototype) // true
console.log(RegExp.prototype.__proto__ === Object.prototype) // true
console.log(Error.prototype.__proto__ === Object.prototype) // true
复制代码
值类型也能访问到
Object
原型链路是因为在使用值类型属性时,js 会隐式转换使用包装类去访问
const num = 123
console.log(num.__proto__.__proto__ === Object.prototype) // true,相当于包装类Number去访问
复制代码
继承特点(Child 构造函数继承 Father 构造函数,Child 实例化对象 child):
- child 实例内部拥有 Child、Father 构造函数内的公开属性
- child 实例可以访问 Child、Father 原型链路
class Father {
constructor() {
this.father = 'father'
}
}
class Child extends Father {
constructor() {
super()
this.child = 'child'
}
}
Father.prototype.x = 'x'
const child = new Child()
console.log(child) // Child { father: 'father', child: 'child' }
console.log(child.x) // x
复制代码
功能实现
由继承的两个特点可知:
- 为了让子实例内部同时拥有子、父构造函数内公开的属性,则需要在 new 实例的时候执行一遍父构造函数与子构造函数,由于 new 默认执行子构造函数,则需要在子构造函数中去调用一次父构造函数(需要将 this 绑定至实例)
- 为了让子实例能访问到父构造函数原型链,则需要建立原型链连接,参考 Object 与其他子类的原型链连接可知,令
子原型 = 父实例
即可
function Father() {
this.father = 'father'
}
function Child() {
if(Child.extendsFn) {
Child.extendsFn.call(this)
}
this.child = 'child'
}
const _extends = (Father, Child) => {
Child.prototype = new Father() // 绑定原型链
Child.extendsFn = Father // 让子构造函数内部得以执行父构造函数
}
_extends(Father, Child)
Father.prototype.x = 'x'
const child = new Child()
console.log(child) // Father { father: 'father', child: 'child' },注意这里的标志是Father
console.log(child.x) // x
复制代码
此时原型链关系:
// 实例与子父构造函数原型链连接
console.log(child instanceof Child) // true
console.log(child instanceof Father) // true
// 缺失一些原型链造成的原型链路混乱,js认为child的构造函数为Father
console.log(child.constructor === Father) // true
console.log(child.__proto__.constructor === Father) // true
// 父构造函数自身的原型链路正常
console.log(child.__proto__.__proto__ === Father.prototype) // true
console.log(child.__proto__.__proto__.constructor === Father) // true
复制代码
上面的实现思路存在两个问题:
- 在实例化子对象的时候,没有必要去实例化父对象
- 由于没有绑定
子原型.constructor = 子构造函数
原型链路,导致子原型链错误
为了解决这个问题,需要把父级构造函数从继承原型链中去除,直接方法是不使用父构造函数去 new 实例,同时绑定 子原型.constructor = 子构造函数
即可
const _extends = (Father, Child) => {
Child.prototype = Object.create(Father.prototype) // 去除父级构造函数的关联,直接建立子原型=父实例
Child.prototype.constructor = Child // 建立子原型-→子构造函数的关系
Child.extendsFn = Father // 让子构造函数内部得以执行父构造函数
}
复制代码
此时原型链关系:
console.log(child) // Child { father: 'father', child: 'child' }
// 实例与子父构造函数原型链连接
console.log(child instanceof Child) // true
console.log(child instanceof Father) // true
// 子构造函数自身的原型链路正常
console.log(child.constructor === Child) // true
console.log(child.__proto__.constructor === Child) // true
// 父构造函数自身的原型链路正常
console.log(child.__proto__.__proto__ === Father.prototype) // true
console.log(child.__proto__.__proto__.constructor === Father) // true
复制代码