1. js的继承
-
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要继承过来即可
-
而继承的关键是使用JavaScript原型链机制
-
可以使用原型链机制来实现继承
2. JavaScript对象的原型链
JavaScript 中的每个对象都有一个隐藏属性 [[Prototype]](可以通过 __proto__ 访问),它指向另一个对象,我们称之为“原型”。
当你访问对象的某个属性或方法时,JS 引擎会:
- 先查对象本身
- 如果找不到,就沿着它的
__proto__(原型)往上找 - 一直找到顶层
Object.prototype,如果还没有,就返回undefined
这个查找的路径,就是 原型链。
| 关键点 | 说明 |
|---|---|
对象通过 __proto__ 指向其原型 | 查找属性时就会沿着原型链往上查找 |
原型链以 Object.prototype 结束 | 它的 __proto__ 是 null |
prototype 是函数专属 | 只有函数才有 prototype 属性,用于构造实例的原型对象 |
| 多对象共享一个原型 | 可以节省内存,方法共享 |
原型链是有终点的。且终点的指向为NUll
object的原型
var obj = {name: 'why'}
console.log(obj.__proto__) //[Object: null prototype] {}
//看到[Object: null prototype] {} 说明已经到了obj这个对象的最顶层的原型了
console.log(obj.__proto__.__proto__) //null
//可以看出最顶层的原型的 __proto__ 为 null
顶层的原型
方法1:var obj1 = {}
方法2:
var obj2 = new Object()
方法1是方法2的语法糖形式
var obj1 = {}
var obj2 = new Object()
console.log(obj1.__proto__ === obj2.__proto__) //true
本质都是:obj1.__proto__ = Object.prototype |
obj2.__proto__ = Object.prototype
console.log(obj1.__proto__) //[Object: null prototype] {}
console.log(obj1.__proto__.__proto__) //null
console.log(obj2.__proto__) //[Object: null prototype] {}
console.log(obj2.__proto__.__proto__) //null
可以看到Object的函数原型对象是obj1 和 obj2的对象原型链的顶层了
此时我们可以把 var obj2 = new Object()中的Object()对于对象来说当成一个特殊的函数
当我们改变了obj2的__proto__会发生什么
当obj2 中找不到相关的属性,obj2会沿着原型链obj2 -> obj1 -> Object的原型对象 -> __proto__ : null。找到就返回,找不到就返回undefined
那么,Object()和我们之前的Person()被称为构造函数有什么区别呢?
我们可以看到Object的原型对象有__proto__,那么Person函数的原型对象是否也有__proto__
答案:Person函数的原型对象 有 __proto__
function Person() {
}
var obj1 = new Object()
var obj2 = new Person()
那么Person的原型对象的__proto__属性指向哪里?
function Person() {
}
var obj1 = new Object()
var obj2 = new Person()
console.log(Object.prototype === obj1.__proto__) //true
console.log(Object.prototype === Person.prototype.__proto__) //true
console.log(obj1.__proto__ === Person.prototype.__proto__) //true
3. 怎么使用原型链实现继承
使用父类:存放公共的属性和方法
使用子类:存放特有的属性和方法
function Father() {
this.name = 'why'
}
Father.prototype.eating = function() {
console.log(this.name + 'eating😮')
}
var father = new Father()
function Son() {
this.son = 111
}
Son.prototype.studying = function () {
console.log(this.name + 'studying📚')
}
var sonOne = new Son()
console.log(sonOne.name)
console.log(sonOne.studying)
我们的代码较少,在很多代码中。我们的sonOne对象想要通原型链,在自己没有特定属性的情况下,可以使用某种方法找到fatherOne对象中的属性和Father的默认原型对象
function Father() {
this.name = 'why'
}
Father.prototype.eating = function() {
console.log(this.name + 'eating😮')
}
var father = new Father()
//增加的代码(只有这一行)
Son.prototype = father
function Son() {
this.son = 111
}
Son.prototype.studying = function () {
console.log(this.name + 'studying📚')
}
var sonOne = new Son()
console.log(sonOne.name)
console.log(sonOne.studying)
这样sonOne中没有的对象就可以沿着原型链查找fatherOne对象和Father函数默认原型对象了
4. 原型链继承的弊端
代码1
function Father() {
this.name = 'why'
this.friends = []
}
Father.prototype.eating = function() {
console.log(this.name + 'eating😮')
}
var father = new Father()
Son.prototype = father
function Son() {
this.son = 111
}
Son.prototype.studying = function () {
console.log(this.name + 'studying📚')
}
var sonOne = new Son()
var sonTwo = new Son()
sonOne.name = 'kobe'
console.log(sonOne)
console.log(sonTwo)
sonOne.friends.push('kebo')
console.log(sonOne.friends)
console.log(sonTwo.friends)
//Father { son: 111, name: 'kobe' }
//Father { son: 111 }
//[ 'kebo' ]
//[ 'kebo' ]
解释:
-
sonOne.name = 'kobe' :
- 当你给 sonOne.name 赋值时,JavaScript 引擎首先查找 sonOne 对象自身是否有 name 属性。它没有。
- 因为是赋值操作 (=) ,引擎不会去修改原型链上的属性。相反,它会在sonOne 对象自身上创建一个新的 name 属性,并赋值为 'kobe'。这叫做属性屏蔽 (Property Shadowing)。
- 此时 sonOne 变成: { son: 111, name: 'kobe', proto: father }
- sonTwo 保持不变: { son: 111, proto: father }
-
console.log(sonOne) : 输出 sonOne 对象,它现在有自己的 name 属性,值为 'kobe'。
-
console.log(sonTwo) : 输出 sonTwo 对象,它没有自己的 name 属性。当你访问 sonTwo.name 时,会沿着原型链找到 father 对象上的 name 属性,其值为 'why'。(虽然 console.log(sonTwo) 可能不会直接显示原型链上的属性,但访问 sonTwo.name 会得到 'why')。
-
这就是为什么前两个打印不一样的原因。
-
sonOne.friends.push('kebo') :
-
当你访问 sonOne.friends 时,JavaScript 引擎首先查找 sonOne 自身是否有 friends 属性。它没有。
-
于是引擎沿着原型链查找,在 sonOne.proto(也就是 father 对象)上找到了 friends 属性。这个 friends 属性引用的是上面提到的那个具体的数组实例 Array A。
-
所以 sonOne.friends 解析为 father.friends,也就是 Array A。
-
.push('kebo') 操作是直接修改了这个 Array A 数组本身,在数组末尾添加了 'kebo'。
-
现在 father 对象(也就是 sonOne 和 sonTwo 共享的原型)变成了:
{ name: 'why', friends: ['kebo'], // Array A 被修改了! studying: function(){...}, __proto__: Father.prototype }
-
-
console.log(sonOne.friends) : 访问 sonOne.friends,沿着原型链找到 father.friends,它现在是 ['kebo']。
-
console.log(sonTwo.friends) : 访问 sonTwo.friends,同样沿着原型链找到 father.friends,它也是 ['kebo']。
-
这就是为什么后两个打印一样的的原因。
核心问题总结:
这种继承方式(将子类的原型直接指向父类的一个实例)会导致:
- 父类实例的属性(如 name, friends)被所有子类实例共享。
- 对于原始类型(如 string, number)的属性,当子类实例尝试修改它们时,会在子类实例自身创建新属性(屏蔽),看起来像是独立的。
- 对于引用类型(如 Array, Object)的属性,所有子类实例访问的都是原型上同一个引用。当任何一个子类实例通过这个引用修改了对象或数组内部时,这个修改对所有其他子类实例都可见,因为它们共享同一个引用。
代码2
function Father() {
this.name = 'why'
}
Father.prototype.eating = function() {
console.log(this.name + 'eating😮')
}
var father = new Father()
Son.prototype = father
function Son() {
this.son = 111
}
Son.prototype.studying = function () {
console.log(this.name + 'studying📚')
}
var sonOne = new Son()
var sonTwo = new Son()
sonOne.name = 'kobe'
console.log(sonOne)
console.log(sonTwo)
sonOne.friends = ['aaa']
console.log(sonOne.friends)
console.log(sonTwo.friends)
//Father { son: 111, name: 'kobe' }
//Father { son: 111 }
//[ 'aaa' ]
//undefined
代码解释
-
sonOne.name = 'kobe' :
- 和上一个例子逻辑相同:赋值操作 (=) 在 sonOne 上查找 name 属性,未找到。
- 于是,在 sonOne 实例自身上创建了一个新的 name 属性,并赋值为 'kobe'(属性屏蔽)。
- sonOne 变为: { son: 111, name: 'kobe', proto: father }
- sonTwo 保持不变: { son: 111, proto: father }
-
console.log(sonOne) : 打印 sonOne,它有自己的 name: 'kobe'。
-
console.log(sonTwo) : 打印 sonTwo。如果访问 sonTwo.name,它会通过原型链找到 father 上的 'why'。
-
结果: 前两个打印结果不同,原因和之前一样(对实例属性的赋值创建了自身属性)。
-
sonOne.friends = ['aaa'] :
- 这是与上一个例子 (.push) 的关键区别。
- JavaScript 引擎在 sonOne 上查找 friends 属性。未找到。
- 因为这是一个赋值操作 (=) ,引擎不会去原型链上查找并修改。相反,它会在sonOne 对象实例自身上创建一个全新的 friends 属性,并将一个新的数组 ['aaa'] 赋值给它。
- sonOne 现在变为: { son: 111, name: 'kobe', friends: ['aaa'], proto: father }
- sonTwo 保持不变: { son: 111, proto: father }
- 共享的原型对象 (father) 完全没有受到这次赋值的影响。
-
console.log(sonOne.friends) :
- 访问 sonOne.friends 时,直接在 sonOne 实例上找到了该属性。
- 输出: ['aaa']
-
console.log(sonTwo.friends) :
- 访问 sonTwo.friends。
- 在 sonTwo 实例上查找 friends。未找到。
- 去 sonTwo.proto(即 father 对象)上查找 friends。在这个版本的代码里,father 对象自身也没有 friends 属性。未找到。
- 去 father.proto(即 Father.prototype)上查找 friends。未找到。
- 去 Father.prototype.proto(即 Object.prototype)上查找 friends。未找到。
- 到达原型链末端。
- 输出: undefined
结论:
在这个修改后的版本中:
- 对 sonOne.name 的赋值仍然导致 sonOne 和 sonTwo 在 name 属性上表现不同(一个有自身属性,一个继承原型属性)。
- 对 sonOne.friends 进行赋值 (= ['aaa']),创建的是 sonOne 实例自身的属性,并没有修改共享的原型。
- 由于 sonTwo 没有自身的 friends 属性,并且这次共享的原型 father 上也不存在 friends 属性,因此访问 sonTwo.friends 的结果是 undefined。