最近再次翻看了《JavaScript高级程序设计》和 《阮一峰 ES6》复习了关于原型,对象,继承这部分的知识。并做了一些归纳总结。这篇文章算是读书的笔记。会从一下几个方面在总结这部分的知识。
目录
(一) 对原型和原型链的理解
(二) 原型和原型链相关的练习题目
(三) ES5创建对象的方法和继承的各种实现方式以及优劣
(四) ES6中使用Class创建对象和实现继承的原理
一、对原型和原型链的理解
1、理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype
属性(指向原型对象)。这个对象包含了可以让构造函数所有实例都共享的属性和方法。 所有原型对象自动获得一个名为constructor
的属性,指回与之关联的构造函数。
每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]
指针就会被赋值为构造函数的原型对象。 脚本中没有访问这个[[Prototype]]
特性的标准方法,但Firefox、Safari和Chrome会在每个对象上暴露__proto__
属性,通过这个属性可以访问对象的原型。
重点理解:实例与构造函数的原型之间有直接的联系。
function Persion() {}
Presion.prototype.constructor === Persion // true
const p = new Persion();
p.__proto__ === Persion.prototype;
复制代码
2、理解原型链
由于原型本身也是个对象,所以内部也会有__proto__
属性,指向Object.prototype.
。当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去它的原型对象里找这个属性,原型对象又会有自己的原型,于是就会这样一直找下去,也就是原型链的概念了。
对象访问属性的顺序:搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
原型链的问题: 由于原型链上的属性和方法是所有的实例共享的,当原型中包含引用值的时候,在一个实例上修该了这个值,其他的实例也会受到影响。
所以定义属性要在构造函数中定义,不在原型上定义。
Persion.prototype.__proto__ === Object.prototype;
总结:
理解原型重要的是明白:构造函数、实例对象、原型 之间的关系
3、总结一些和原型相关的方法
(1)isPrototypeOf()
确定两个对象只爱你的关系。会在传入参数的[[Prototype]]
指向调用的对象时返回true
Persion.prototype.isPrototypeOf(p1);
Persion.prototype.isPrototypeOf(p2);
复制代码
(2)Object.getPrototypeOf()
返回参数的内部特性 [[Prototype]]
的值。
Object.getPrototypeOf(p1) === Persion.prototype;
复制代码
(3)Object.setPrototypeOf()
, 可以向实例的私有特性[[Prototype]]
写入一个新值,这样可以重写一个对象的原型继承关系。
但是会影响到代码的性能。 可通过 Object.create()
来创建对象,并制定原型。 (只做了解,关于Object.create后续在说)
(4)hasOwnPrototype()
方法用于确定某个属性是在实例上还是在原型对象上。 方法继承自Object,当属性存在于调用它的对象实例上时返回true。
(5)in
操作符。单独使用 和 for-in
循环中使用。 In
会在可以通过对象访问指定属性时返回true, 无论这个属性是在实例上还是在原型上。
使用 for-in
循环要求 属性是可枚举的。
(6) Object.keys()
接收一个对象作为参数,返回包含该对象所有可枚举的属性名的字符串数组。
(7)Object.getOwnPropertyNames()
列举所有实例属性,无论是否可以枚举
二、原型和原型链相关的练习题目
题目1:
function Persion() {};
Persion.name = 'zhouzhou';
const p1 = new Persion();
p1.__proto__ === Persion.prototype; // true
Object.getPrototypeOf(p1) === Persion.prototype; // true
Persion.prototype.constructor === Persion;
复制代码
题目2:
function Persion() { }
const p = new Persion();
console.log(p.__proto__)
console.log(Persion.prototype.__proto__)
console.log(p.__proto__.__proto__)
console.log(p.__proto__.constructor.prototype.__proto__)
console.log(Persion.prototype.constructor.prototype.__proto__)
console.log(p.__proto__.constructor)
console.log(Persion.prototype.constructor)
复制代码
正确输出:
// Persion.prototype
// Object.prototype
// Object.prototype
// Object.prototype
// Object.prototype
// Persion
// Persion
复制代码
题目3
function Persion() {}
Persion.prototype.say = function() {
console.log('persion say');
}
function Animal() {}
Animal.prototype = {
say: function() {
console.log('animal say');
}
}
const persion1 = new Persion();
persion1.say();
console.log('persion1--constructor-1--', persion1.__proto__ === Persion.prototype);
console.log('persion1--constructor-2--', persion1.__proto__ === persion1.constructor.prototype);
console.log('persion1--constructor-3--', persion1.constructor === Persion);
console.log('persion1--constructor-4--', persion1.constructor === Object);
const animal1 = new Animal();
animal1.say();
console.log('animal--constructor-1--', animal1.__proto__ === Animal.prototype);
console.log('Animal.prototype-----', Animal.prototype);
console.log('animal1.constructor.prototype-----', animal1.constructor.prototype);
console.log('animal--constructor-2--', animal1.__proto__ === animal1.constructor.prototype);
console.log('animal--constructor-3--', animal1.constructor === Animal);
console.log('animal--constructor-4--', animal1.constructor === Object);
复制代码
正确输出
// persion say
// persion1--constructor-1-- true
// persion1--constructor-2-- true
// persion1--constructor-3-- true
// persion1--constructor-4-- false
// animal say
// animal--constructor-1-- true
// Animal.prototype----- {say: function: {}, [[Prototype]]: Object}
// animal1.constructor.prototype----- {constructor: ƒ Object()...}
// animal--constructor-2-- false
// animal--constructor-3-- false
// animal--constructor-4-- true
复制代码
问题解析:
Animal.prototype
被覆盖了
注意覆盖的方式,animal1
的构造函数 animal1.constructor
就不在指向 Animal
了。指向了Object
。导致无法使用constructor
判断类型了。
animal1
上其实没有constructor
属性,constructor
属性是在 Animal.prototype
属性上的。现在Animal.prototype
被覆盖了,所以指针的指向就发生了变化。
修改上面的问题
更改下constructor
的指向。
Animal.prototype = {
constructor: Animal,
say: function() {
console.log('animal say');
}
}
console.log('animal-fix-constructor-3--', animal1.constructor === Animal); // true
console.log('animal-fix-constructor-4--', animal1.constructor === Object); // false
复制代码
题目4:
function AnimalOne() {}
let animal2 = new AnimalOne();
AnimalOne.prototype = {
constructor: AnimalOne,
say: function() {
console.log('animal say');
}
}
animal2.say();
复制代码
正确输出
// animal1.say is not a function
复制代码
问题解析:
在new
之后重新覆盖了原型对象。切断了构造函数和最初原型对象之的关系。
实例对象中 __proto__
一直指向的是最初的原型对象,现在原型被重写了,相当于在堆内存中另起了一块内存,
原型指针的指向也发生了变化,指向了新的内存,但是实例中__proto__
这个指针还是最初的那一块内存中的原型对象。
修改的方法
animal2.__proto__ = AnimalOne.prototype;
复制代码
题目5
function Foo () {
getName = function () {
console.log(1);
}
return this;
}
Foo.getName = function () {
console.log(2);
}
Foo.prototype.getName = function () {
console.log(3);
}
var getName = function () {
console.log(4);
}
function getName () {
console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
复制代码
正确输出
// 结果:2,4,1,1,2,3,3
复制代码
解析题目:
先进行预编译:
全局对象: function Foo, var getName, function getName() {}
把 变量getName
覆盖了
(1)function Foo () {getName = function(){}}:
全局作用域中存在一个函数Foo
,当函数执行后可以获取到函数内部的 getName
函数,此时 getName
也会升级为全局作用域中的变量。
(2)Foo.getName = function (){}
函数对象上挂载属性 getName
这个只能有Foo
进行访问,无法通过new
继承到实例对象上去。
(3)Foo.prototype.getName = function (){}
在函数的原型上挂载了 getName
属性。当Foo
的实例对象去访问时,可以通过原型链找到这个方法
(4)var getName = function(){}
全局变量声明被function getName
覆盖了,此时进行变量赋值,被变量声明的函数再次覆盖了。
(5)function getName () {} function
声明的函数也会全局提升到全局,不过这里是整体提升。这里就跳过不在执行。
(6)Foo.getName()
通过(2)的分析,得到 这里输出的是 2
(7)getName()
当经过预编译和代码一行一行执行,到这里进行调用时,此时全局getName对应的是变量声明的那个函数。输出 4
(8)Foo().getName()
此时Foo()
函数执行,然后调用了内部的getName
函数,此时输出 1.
同时这个 getName
会覆盖全局的已经存在的getName。
(9)getName()
此时由于全局的getName
被 Foo
函数内的getName
覆盖了,所以输出 1.
(10)new Foo.getName()
这里先执行 Foo.getName()
,在执行new
输出 2
(11)new Foo().getName();
先执行(new Foo())
得到实例对象,在去调用 getName
由于在function Foo
中的getNanme
不是挂载在this
上的,所以不能通过new
继承。Foo.getName
只能通过 Foo
访问到也无法通过new
继承。只能通过原型链去查找, 于是输出 3
(12)new new Foo().getName();
同上 new (new Foo().getName())
输出3
题目六
var F = function() {};
Object.prototype.a = function() {
console.log('a');
};
Function.prototype.b = function() {
console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b()
复制代码
运行结果:
// f.a() a
// f.b() f.b is not a function
// 移除f.b 继续执行
// F.a() a
// F.b() b
复制代码
解析: 可参考文章中的原型链的图示
参考学习文章: blog.csdn.net/lll_y1025/a…
题目七
function Foo() {
Foo.a = function() {
console.log(1);
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3);
}
Foo.a = function() {
console.log(4);
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
复制代码
正确的输出: 4, 2 1
解析:
Foo.a()
由于function Foo
还没有被执行。所以这里输出的是4
obj.a()
obj
是 function Foo
的一个实例,且a
是挂载在 function Foo
上的属性。 所以这里输出 2
Foo.a()
此时 function Foo
已经执行,Foo.a
就覆盖了下面的 Foo.a
这里输出的是 1
题目八
function Dog() {
this.name = 'puppy'
}
Dog.prototype.bark = () => {
console.log('woof!woof!')
}
const dog = new Dog()
console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog);
// true
复制代码
题目九
var A = {n: 4399};
var B = function(){this.n = 9999};
var C = function(){var n = 8888};
B.prototype = A;
C.prototype = A;
var b = new B();
var c = new C();
A.n++
console.log(b.n);
console.log(c.n);
复制代码
正确输出:
9999 4400
重点注意的是,查找的顺序,以及构造函数的那些属性和方法可以被实例对象访问。
题目十
function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a);
console.log(new B().a);
console.log(new C(2).a);
复制代码
正确输出
1, undefined, 2
解析:
注意查找的顺序,优先查看实例对象自身的属性上是否包含,如果没有再去看原型上的
三、 ES5创建对象的方法和继承的各种实现方式以及优劣
ES5创建对象的方法以及优劣
1、工厂模式
2、构造函数模式
3、原型链模式
4、组合模式
1. 工厂模式
function createPerson(name) {
var o = new Object();
o.name = name;
o.getName = function () {
console.log(this.name);
};
return o;
}
var person1 = createPerson('kevin');
复制代码
缺点:对象无法识别,因为所有的实例都指向一个原型
2. 构造函数模式
function Person(name) {
this.name = name;
this.getName = function () {
console.log(this.name);
};
}
var person1 = new Person('kevin');
复制代码
优点:实例可以识别为一个特定的类型
缺点:每次创建实例时,每个方法都要被创建一次
3. 原型模式
function Person(name) {}
Person.prototype = {
constructor: Person, // 注意这里的指向
name: 'kevin',
getName: function () {
console.log(this.name);
}
};
var person1 = new Person();
复制代码
优点:方法不会重新创建,挂载在原型上的方法和属性可以被实例对象共享
缺点:
-
所有的属性和方法都共享。这种共享对于函数来说非常合适,但是对于属性来讲就不太合适了。如果是基本属性,实例可以定义同名的属性,就可以屏蔽了。对于应用类型的值,虽然也可以定义同名属性去屏蔽,但是要想获取初始化的值且在改动是不影响其他的属性就需要深拷贝了。
-
不能初始化参数
4. 组合模式
构造函数模式与原型模式双剑合璧。
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
};
var person1 = new Person();
复制代码
实例属性都是在构造函数中定义的,由所有实例共享的属性都是在原型中定义的。
优点:该共享的共享,该私有的私有,使用最广泛的方式
继承的各种实现方式以及优劣
1、原型链继承; 2、借用构造函数;3、组合继承;4、原型式继承;5、寄生式继承;6、寄生组合式继承;
1、原型链
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subProperty = false;
}
SubType.prototype = new SuperType();
// 子类型需要添加或者重写父类型中的方法,一定要放在替换原型的语句之后
// 且不能通过对象字面量的方式创建原型方法,否则会重写原型
SubType.prototype.getSubValue = function() {
return this.subProperty;
}
const instance = new SubType();
console.log(instance.getSuperValue())
复制代码
解析:
SubType
继承了 SuperType
,继承是通过创建SuperType
的实例,并将该实例赋给SubType.prototype
实现的。
实现的本质是重写原型对象。
那么原来存在于SuperType
的实例中的所有属性和方法现在都在SubType.prototype
中了。并且内部还包含了一个指针指向的是SuperType
的原型。
instance
中的constructor
现在指向的是SuperType
, 因为SubType.prototype
被重写了。
下图是整个原型链
问题:
(1)最主要的是包含引用类型值的原型。
(2)在创建子类型实例时,不能像附列传递参数。
2、借用构造函数
基本思想:在子类型构造函数的内部调用超类型构造函数。目的在于解决原型中包含引用类型值所带来的问题。
function SuperType() {
this.colors = ['red', 'yellow'];
}
SuperType.prototype.getColors = function() {
return this.colors;
}
function SubType() {
SuperType.call(this);
}
const instance = new SubType();
const instance1 = new SubType();
instance.colors.push('black');
instance1.colors.push('orange');
// 代码报错,找不到这个方法
instance.getColors();
instance1.getColors();
console.log(instance.colors);
console.log(instance1.colors);
复制代码
问题:
(1)方法都在构造函数中定义,那么方法的复用就有问题了
(2)在父类型的原型中定义的方法,对子类型是不可见的(无法调用到),结果所有的类型都只能使用构造函数模式。
3、组合继承模式
将原型链和借用构造函数组合起来。使用原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承。
function SuperType() {
this.colors = ['red', 'yellow'];
}
SuperType.prototype.getColors = function() {
return this.colors;
}
function SubType() {
SuperType.call(this);
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
const instance = new SubType();
const instance1 = new SubType();
instance.colors.push('black');
console.log(instance.getColors());
console.log(instance1.getColors());
复制代码
是目前最常用的继承模式。并且通过instanceof 和 isPrototypeOf()
也能够识别基于组合继承创建的对象。
存在的问题点:无论什么情况下,都会调用两次父类构造函数。
4、原型式继承
基本思想:借助原型可以基于已有的对象创建新对象。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
复制代码
先参加一个临时的构造函数,然后将传入的对象作为构造函数的原型,最后在返回这个临时类型的一个新实例。
这个方法,后面通过 Object.create()
方法规范了下来。
Object.create()
接收两个参数,第一个参数:用作新对象原型的对象。 第二个参数:(可选)一个为新对象定义额外属性的对象。
但是这里对包含引用类型值的属性始终都会被共享。
5、寄生式继承
基本思路:和原型式继承密切相关的一种思路。创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后在返回这个对象
function createAnoterh (o) {
var clone = object(o);
clone.sayHi = function () {
console.log(‘say hi’);
}
return clone;
}
复制代码
这种方式会降低复用性
6、寄生组合式继承
基本思路:借用构造函数来继承属性,通过原型链的混成形式来继承方法。 不必为了指定子类型的原型而调用父类的构造函数,我们使用的是父类原型的一个副本
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
const prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType() {
this.colors = ['red', 'yellow'];
}
SuperType.prototype.getColors = function() {
return this.colors;
}
function SubType() {
SuperType.call(this);
}
inheritPrototype(SubType, SuperType);
const instance = new SubType();
const instance1 = new SubType();
instance.colors.push('black');
console.log(instance.getColors());
console.log(instance1.getColors());
复制代码
以上即是在ES5中我们实现创建对象和对象继承的一些方法总结。
四、ES6中使用Class创建对象和实现继承的原理
在ES6中引入了class
关键字,具有正式的定义类的能力。这里Class
看起来像是支持了面向对象的变成,但是背后使用的仍然是原型和构造函数的概念。
类实际上就是一个 funciton
。可以使用typeof
检验。
关于类
1、定义类:
注意类不存在变量提升
// 类声明
class Persion {}
// 类表达式
const Persion = class {}
复制代码
2、类的构成:
构造函数方法,实例方法,获取函数,设置杉树,静态类方法,静态属性
class Persion {
constructor(name, age) {
this.name = name;
this.age = age;
}
/*
constructor 可以省略
实例属性: name = ‘’;
*/
sayHi() {
console.log(‘say, hi’);
}
// 类中定义的方法多可以被实例继承,但是添加static后,这个就是静态方法,不能被实例继承,而是直接通过类来调用的。可以被子类继承
static sayHiByName() {
console.log(‘sayHiByName’)
}
// 静态属性是class本身的属性,不是定义在实例对象(this)上的属性,只能用类来获取。
static userName = ‘zhouzhou’;
get name() {
return ‘zz’;
}
set name(name) {
return `${name}——`;
}
}
// 类必须使用new调用,否则会报错。
const p = new Persion(‘zz’, 18);
p.name = ‘yy’; // yy——
复制代码
对应到ES5
function Persion (name, age) {
this.name = name;
this.age = age;
}
Persion.prototype.sayHi = function() {
console.log(‘say, hi’);
}
Persion.sayHiByName = function () {
console.log(‘sayHiByName’);
}
Persion.userName = ‘zz’;
复制代码
类构造函数:constructor
关键字用于类内创建类的构造函数。
当使用new操作符创建类时会调用者函数。 这个函数不是必须的,不定义时默认会定一个而空函数。以上的对应关系可以借助babel转换进行查看。
3、私有方法和私有属性
私有方法和私有属性是只能在类内部访问的方法和属性,不能在外部访问。但是ES6中没有提供方法,只能通过其他的方式进行模拟。
常用的方法有:利用Symbol
的唯一性,将私有方法的名字命名为一个Symbol值。
提案:在私有属性前面添加 #
关于继承
ES6支持单继承,通过 extends关键字就可以继承任何拥有 [[prototype]]
和原型的对象。extends
背后依旧使用的是原型链。
1、super();
(1)相当于ES5继承中的 Parent.call(this)
(2)在子类中如果显示的调用了constructor
方法,那就必须在内部调用super()
, 否则会报错。主要是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到和父类相同的属性和方法,然后再对其进行加工。如果不调用,子类就无法获取自己的this
对象。
(3)super()
作为函数只能在构造函数中使用。 super()
在子类构造中内部的this指向的是子类。
(4)super
作为对象时,在普通方法和静态方法中都是指向的父类
2、类的prototype 和 __proto__属性
在class
中 父类的静态方法可以被子类继承。主要是因为 class
作为构造函数的语法糖,同时有prototype
属性和__proto__
属性。同时存在两条继承链
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2) 子类的prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class Parent {}
class Child extends Parent {}
Child.__proto__ === Parent
Child.prototype.__proto__ === Parent.prototype
复制代码
参考文章: