本篇为阅读《红宝书》第八、九章节所作笔记,感受js的构造函数、原型等~
创建对象
工厂模式
-
下面的例子展示了一种按特定接口创建对象的方式:
- 可以用不同的参数多次调用这个函数,每次都会返回包含2个属性和1个方法的对象
- 这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)
function createPerson(name, age) {
let o = new Object();
o.name = name;
o.age = oge;
o.sayName = function () {
console.log(this.name);
}
return o
}
let p1 = createPerson('john',18)
let p2 = createPerson('miki',18)
构造函数模式
按照惯例,构造函数名称的首字母都要大写,非构造函数则以小写字母开头
// 工厂模式的例子使用构造函数可以这样子写:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
let p1 = new Person('john', 18)
let p2 = new Person('miki', 18)
p1.sayName() // john
p2.sayName() // miki
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Object); // true
-
ECMAScript中的构造函数是用于创建特定类型对象的,像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用。也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法
-
Person()内部代码和createPerson()基本一样,区别在于
- 没有显示地创建对象
- 属性和方法直接赋值给了
this - 没有return
-
要创建 Person 的实例,应使用
new操作符,以这种方式调用构造函数会执行一下操作:- 在内存中创建一个新对象
- 这个新对象内部的
[[Prototype]]特性被赋值为构造函数的prototype属性 - 构造函数内部的
this被赋值为这个新对象(即this指向新对象) - 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
-
定义自定义构造函数可以确保实例被标识为特定类型,例如以上的p1被认为是Object的实例,是因为所有自定义对象都继承自Object
-
赋值给变量的函数表达式也可以表示构造函数;在实例化时,如果不想传参数,构造函数后面的括号可加可不加,只要有
new操作符,就可以调用相应的构造函数-
let Person = function() {} let p3 = new Person(); let p4 = new Person;
-
构造函数也是函数
-
与普通函数的区别就是调用方式不同
-
任何函数只要使用
new操作符调用就是构造函数function Person(name, age) { this.name = name; this.age = age; this.sayName = function () { console.log(this.name); } } // 作为构造函数 let p1 = new Person('p1', 18); p1.sayName() // p1 // 作为函数调用 结果会将属性和方法添加到 全局对象(window)上 Person('p2', 19) window.sayName(); // p2 // 在另一个对象的作用域中调用 let o = new Object(); Person.call(o, 'p3', 20); // 将对象o指定为Person内部的this值,执行完函数代码后,所有属性和方法都会添加到o上 o.sayName() // p3
构造函数的问题
-
其定义的方法会在每个实例上创建一遍,例如上述的
sayName方法,在p1和p2实例中都会重新创建,即使函数同名且做一样的事情console.log(p1.sayName == p2.sayName); // false -
解决这个问题,可以将函数定义转移到构造函数外部
- 在构造函数内部,sayName属性中包含的只是一个指向外部函数的指针,所以p1和p2共享了定义在全局作用域上的sayName函数
- 虽然解决了相同逻辑的函数重复定义的问题,但是全局作用域因此被搞乱了,因为sayName函数实际上只能在一个对象上调用。如果这个对象需要多个方法,就需要在全局作用域定义多个函数,导致自定义类型引用的代码不能很好地聚集一起,该方法通过原型模式来解决
function Person(name, age) { this.name = name; this.age = age; this.sayName = sayName } function sayName() { console.log(this.name) }
原型模式
-
每个函数都会创建一个
prototype属性,这个属性是一个对象,包含一些共享的属性和方法 -
实际上,这个对象就是通过调用构造函数创建的对象的原型
-
使用原型对象的好处:在它上面定义的属性和方法可以被所有对象实例共享
// 使用函数表达式也可以: let Person = function () { Person.prototype.sayName = function () { console.log('this is prototype.sayName'); } } let p1 = new Person(); p1.sayName() // this is prototype.sayName let p2 = new Person(); p2.sayName() // this is prototype.sayName console.log(p1.sayName === p2.sayName); // true
理解原型
-
只要创建一个函数,就会为这个函数创建一个
prototype属性(指向原型对象) -
默认情况下,所有原型对象自动获取一个名为
constructor的属性,指回与之关联的构造函数 -
在自定义构造函数时,原型对象默认只会获得 constructor属性,其它所有方法都继承Object
-
每次调用构造函数创建一个新实例,这个实例的
[[prototype]]指针会被赋值为构造函数的原型对象。 在Chrome浏览器可以通过__proto__属性访问对象的原型 -
理解:实例与构造函数原型之间有直接联系,实例与构造函数之间没有
- Person.prototype指向原型对象,Person.prototype.constructor指回Person构造函数
- 原型对象包含 constructor属性和其它后来添加的属性
- 两个实例p1和p2都只有一个内部属性指回Person.prototype,而且两者都与构造函数没有直接联系
- 注意:虽然p1和p2都没有属性和方法,但是可以调用sayName方法,是由于对象属性查找机制的原因
- 可以通过
isPrototypeOf()方法确定两个对象之间的关系
console.log(Person.prototype.isPrototypeOf(p1)); // true console.log(Person.prototype.isPrototypeOf(p2)); // true // 因为这两个例子内部都有链接指向Person.prototype 所以都返回true
-
Object类型有
Object.getPrototypeOf()方法,返回参数的内部特性 [[prototype]] 的值 -
避免使用 Object.setPrototypeOf(),可以通过
Object.create()来创建一个新对象,同时为其指定原型-
console.log(Object.getPrototypeOf(p1)); // {sayName: ƒ, constructor: ƒ} console.log(Object.getPrototypeOf(p1) === Person.prototype); // true Object.getPrototypeOf(p1).sayName() // this is prototype.sayName
-
原型层级
-
通过对象访问属性时,会先在对象实例本身搜索,如果没找到这个属性,则会沿着指针进入原型对象,若找到对应属性,则返回对应的值,p1和p2调用sayName都是同意的搜索过程,这就是原型用于在多个对象实例间共享属性和方法的原理
-
只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性,虽然不会修改到原型对象上的属性值,但是搜索过程在对象实例中找到就不会到原型对象上查找。
- 即使把该属性设置为
null,也是会遮蔽 - 可以使用
delete操作符完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象
- 即使把该属性设置为
-
hasOwnProperty()方法用于确定某个属性是在实例上还是原型对象上,存在实例上返回true,原型对象上返回false-
let Person = function () { Person.prototype.name = 'protoName' } console.log(p1.name); // protoName console.log(p1.hasOwnProperty('name')); // false 此时name存在于原型对象 p1.name = 'p1Name' console.log(p1.name); // p1Name console.log(p1.hasOwnProperty('name')); // true 此时name存在于实例上 p1.name = null; console.log(p1.name); // null delete p1.name; console.log(p1.name); // protoName
-
原型和in操作符
单独使用
-
in操作符会在可以通过对象访问指定属性时返回 true -
基于
hasOwnProperty()返回false 和in返回true 可以确定某个属性是否存在于原型上-
let Person = function () { Person.prototype.name = 'protoName' } console.log('name' in p1); // true console.log(p1.hasOwnProperty('name')); // false 此时name存在于原型对象
-
在 for...in 中使用in操作符
-
可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性
-
Object.keys()方法可以获得对象上所有可枚举的实例属性,该方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组-
p1.name = 'p1Name'; p1.age = 18 console.log(Object.keys(Person.prototype)); // ['name', 'sayName'] console.log(Object.keys(p1)); // ['name', 'age'] for (let item in p1) { console.log(item); } // name age sayName console.log(Object.getOwnPropertyNames(Person.prototype)); // ['constructor', 'name', 'sayName'] console.log(Object.getOwnPropertyNames(p1)); // ['name', 'age']
-
继承
原型链
-
ECMAScript-262把
原型链定义为ECMAScript的主要继承方式,基本思想:通过原型继承多个引用类型的属性和方法,如下代码示例- 我们定义了两个类型 a b,它们的主要区别是 b 通过创建 a 的实例并将该实例赋值给 b 的原型,实现对 a 的继承
- 这个赋值重写了 b 最初的原型,这意味着 a 实例可以访问的所有属性和方法也会存在于
b.prototype - 这样实现继承后,又给 b 的原型(也是a的实例)添加了一个新方法,最后创建 b 的实例 c 并调用它继承的getAValue方法
function a() { this.aName = 'a' } a.prototype.getAValue = function () { return this.aName } function b() { this.bName = 'b' } // 继承a b.prototype = new a() b.prototype.getBValue = function () { return this.bName } let c = new b() console.log(c.getAValue()); // a console.log(c.constructor); // f a()- 注意:getAValue方法还在a原型上,而 aName属性则在b原型上,这是因为getAValue是一个原型方法,而aName是一个实例属性,b.prototype现在是a的一个实例,因此aName才会存储在它上面
- 还要注意:b.prototype的 constructor 被重写为指向 a,所以c.constructor也指向a
-
默认原型
- 所有引用类型都继承自
Object,任何函数的默认原型都是一个Object的实例,这意味着这个实例有一个内部指针指向Object.prototype
- 所有引用类型都继承自
-
原型与继承关系
可以通过两种方式来确定
instanceof
- 如果一个实例的原型链上出现过相应的构造函数,则返回true
console.log(c instanceof Object); // true
console.log(c instanceof a); // true
console.log(c instanceof b); // true
isPrototypeOf
- 原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,则返回true
console.log(Object.prototype.isPrototypeOf(c)); // true
console.log(a.prototype.isPrototypeOf(c)); // true
console.log(b.prototype.isPrototypeOf(c)); // true
- 关于方法
- 子类有时候需要覆盖父类的方法,或者增加父类没有的方法,必须在原型赋值后再添加到原型上
类
类定义
- 两种定义方式
类声明
Class Person {}
类表达式
Const Person = class {}
-
函数声明可以提升,但是类定义不能
-
函数受函数作用域限制,类受块作用域限制
-
console.log(a); function a() { } // f a(){} console.log(b); class b { } // Error: Cannot access 'b' before initialization { function c() { } class d { } } console.log(c); // f c(){} console.log(d); // Error: Cannot access 'd'
-
-
类的构成
- 可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,这些都是非必需的
- 一般建议类名首字母大写
类构造函数
-
通过
constructor关键字在类定义块内创建类的构造函数,非必需 -
通过
new操作符实例化,使用new调用类的构造函数会执行如下操作:- 在内存中创建一个新对象
- 新对象内部的
[[prototype]]指针指向构造函数的prototype属性 - 构造函数中的
this指向该新对象 - 执行构造函数内部的代码(给新对象添加属性)
- 若构造函数返回非空对象,则返回该对象,否则返回刚创建的对象
-
实例化传入的参数会用作构造函数的参数,如果不需要参数,则类名后面的括号也是可选的
class Person { constructor(name) { console.log(arguments.length); this.name = name || null } } let p1 = new Person; console.log(p1.name); // 0 null let p2 = new Person(); console.log(p2.name); // 0 null let p3 = new Person('p3Name'); console.log(p3.name); // 1 p3Name -
类构造函数 与 构造函数 的主要区别
- 调用类构造函数必须使用
new操作符,如果没有使用new会抛出错误 - 普通构造函数如果不使用new调用,会以
全局的this(通常为window)作为内部对象
- 调用类构造函数必须使用
实例、原型和类成员
-
实例成员
-
每个实例都对应一个唯一的成员对象,这意味着所有成员不会在原型上共享
class Person { constructor() { this.name = new String('Jack'); this.sayName = () => console.log(this.name); this.chooseName = ['one', 'two'] } } let p1 = new Person(); let p2 = new Person(); p1.sayName(); // String{'Jack'} p2.sayName(); // String{'Jack'} console.log(p1.name === p2.name, p1.sayName === p2.sayName, p1.chooseName === p2.chooseName); // false false false p1.name = p1.chooseName[0]; p2.name = p2.chooseName[1]; p1.sayName(); // one p2.sayName(); // two
-
-
原型方法与访问器
-
在类块中定义的方法作为原型方法,在实例间可以共享
class Foo { constructor() { // 添加到this的所有内容都会存在于不同的实例上 this.locate = () => console.log('instance'); } locate() { console.log('prototype'); } } let f = new Foo(); f.locate(); // instance Foo.prototype.locate(); // prototype -
类定义支持获取和设置访问器,语法与行为和普通对象一样
-
-
静态类方法
- 静态成员在类定义中使用
static关键字作前缀,其内部this引用类自身 - 静态成员每个类上只能有一个
- 静态成员在类定义中使用
继承
-
使用
extends关键字- 可以继承任何拥有 [[Construct]] 和原型的对象,这意味着不仅可以继承一个类,也可以继承普通的构造函数
class Person { identify() { console.log(this) } } class Bus extends Person { } let b = new Bus(); console.log(b instanceof Bus); // true console.log(b instanceof Person); // true b.identify() // Bus {} -
派生类的方法可以通过
super关键字引用它们的原型- 该关键字只能在派生类中使用,并且仅限于类构造函数、示例方法、静态方法内部
- 在派生类构造函数中使用super关键字可以调用父类构造函数
class Bus extends Person { constructor() { super(); // 不要在调用super之前引用this,否则抛出异常 console.log(this); // Bus {} console.log(this instanceof Person); // true } } -
抽象基类
- 通过
new.target保存通过new关键字调用的类或函数,通过在实例化时检测 new.target 是不是抽象基类,可以实现本身不被实例化,但可供其他类继承
class Vehicle { constructor() { console.log(new.target); if (new.target === Vehicle) { throw new Error('no new self') } } } class v extends Vehicle { }; new v() // class v extends Vehicle new Vehicle(); // class Vehicle -- Error: no new self - 通过
总结
- 构造函数、原型、实例间的关系:每个构造函数都有一个原型对象,原型中存在一个
constructor属性指回构造函数,而实例中有一个内部指针(例Chrome暴露出的__proto__)指向原型 - 原型模式通过添加在构造函数的prototype上的属性和方法,实现实例间共享
- JS的继承主要通过原型链来实现,将构造函数的原型赋值为另外一个类型的实例,这样子,子类即可访问父类所有的属性和方法,就像基于类的继承。问题是无法做到实例私有。
- 类是基于原型机制的语法糖,其构造函数上的属性和方法为实例私有,其他实例间共享,通过
extends关键字可以继承类 - 明白 new 操作符实例一个对象的过程,在内存中创建一个新对象,赋值[[prototype]],绑定this