构造函数
什么是构造函数
构造函数与普通函数的唯一区别就是调用方式不同。构造函数也是函数,并没有把某个函数定位为构造函数的特殊语法,任何函数只要使用new操作符调用就是构造函数, 直接调用的就是普通函数。按照惯例,构造函数的首字母一般要大写。
function Person(name) {
this.name = name;
}
const p = new Person('doudou');
这个过程都发生了什么呢?
- 首先,在内存中创建了一个空对象
- 这个新对象内部的[[Prototype]]属性被赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)
- 执行构造函数内部的代码(给新对象添加属性和方法)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
通过new方式创建的实例对象都有一个constructor属性,这个属性指向实例对象的构造函数。
p.constructor === Person; // true
p.constructor === Object; // false
那是不是意味这普通函数创建的实例没有constructor属性呢?不一定
// 普通函数
function parent2(age) {
this.age = age;
}
var p2 = parent2(50);
// undefined
// 普通函数
function parent3(age) {
return {
age: age
}
}
var p3 = parent3(50);
p3.constructor === Object; // true
constructor是只读的吗?
- 对于引用类型来说contructor属性值使可以修改的
- 对于基本类型来说是只读的
引用类型contructor属性值使可以修改比较好理解,比如原型链继承方案中,就需要对constructor属性重新赋值进行修正。
// 原型链继承
function SuperType () {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType () {
this.subProperty = false
}
// 继承SuperType
SubType.prototype = new SuperType();
// 修正constructor指向
SubType.prototype.constructor = SubType;
SubType.prototype.getSubValue = function () {
return this.subProperty
}
const instance = new SubType();
console.log(instance.constructor === SubType); // true
console.log(instance.getSuperValue()) // true
对于基本类型来说是只读的,比如1、"doudou"、true、Symbol,当然null和undefined是没有constructor属性的。
function Type() { };
var types = [1, "doudou", true, Symbol(123)];
for(var i = 0; i < types.length; i++) {
types[i].constructor = Type;
types[i] = [ types[i].constructor, types[i] instanceof Type, types[i].toString() ];
};
console.log( types.join("\n") );
// function Number() { [native code] }, false, 1
// function String() { [native code] }, false, doudou
// function Boolean() { [native code] }, false, true
// function Symbol() { [native code] }, false, Symbol(123)
为什么呢?因为创建他们的是只读的原生构造函数(native constructors)。
模拟实现new
原型
五个规则
先来了解五个规则:
- 所有引用类型,都具有对象特性,即可自由扩展属性(可动态的添加属性和方法)
- 所有引用类型,都有一个__proto__属性(隐式原型),属性值是一个普通对象
- 所有函数,都有一个prototype属性,属性值也是一个普通对象
- 所有引用类型,__proto__的属性值指向它的构造函数的prototype属性值
- 当你试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型__proto__(也就是它的构造函数的显式原型prototype)中寻找
prototype
JavaScript是一种基于原型的语言,这个和其他基于类的语言不一样。
- 所有的函数,都有一个prototype属性(显式原型),属性值是一个普通对象。
从上面图中可以发现,Person对象有一个原型对象Person.prototype, 它的属性值是一个对象,包含两个属性,分别是constructor和[[Prototype]]。
构造函数Person有一个指向原型的指针,原型Person.prototype有一个指向构造函数的指针Person.prototype.constructor, 如下图所示:
__proto__
- 所有引用类型(数组、对象、函数),都有一个__proto__属性(隐式原型),属性值也是一个普通对象。
- 所有引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值。
function Person() {}
var p = new Person();
p.__proto__ === Person.prototype // true
这里用p.__proto__获取对象的原型,__proto__是每个实例上都有的属性,property是构造函数的属性,这两个并不一样,但p.__proto__和Person.prototype指向同一个对象。
构造函数Person、Person.prototype和实例p的关系如下图:
注意
proto 属性在 ES6 时才被标准化,以确保Web浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()。
如果要读取或修改对象的 [[Prototype]]属性,建议使用如下方案,但是此时设置对象的[[Prototype]]依旧是一个缓慢的操作,如果性能是一个问题,就要避免这种操作。
// 获取
Object.getPrototypeOf()
Reflect.getPrototypeOf()
// 修改
Object.setPrototypeOf()
Reflect.setPrototypeOf()
如果要创建一个新对象,同时继承另一个对象的 [[Prototype]] ,推荐使用 Object.create()。
function Person() {
age: 50
};
const p = new Person();
const child = Object.create(p); // child 是一个新的空对象,有一个指向对象 p 的指针 __proto__
Object.create()的基本实现思路:
function create(obj) {
const F = function() {}
F.prototype = obj
return new F()
}
原型链
当代码读取一个对象的某个属性时,首先会从对象实例本身开始寻找。如果在实例中找到了该属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找该属性,如果在原型对象中找到了该属性,则返回该属性的值,否则继续往上查找。
function Parent(age) {
this.age = age;
}
var p = new Parent(50);
p.constructor === Parent; // true
这里 p.constructor 指向 Parent,那是不是意味着 p 实例存在 constructor 属性呢?并不是。
我们打印下 p 值就知道了。
由图可以看到实例对象 p 本身没有 constructor 属性,是通过原型链向上查找 __proto__ ,最终查找到 constructor 属性,该属性指向 Parent
function Parent(age) {
this.age = age;
}
var p = new Parent(50);
p; // Parent {age: 50}
p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true
下图展示了原型链的运作机制:
原型和实例关系的检测方法——instanceof
instanceof运算符用于检测构造函数的prototype属性是否存在于实例对象的原型链上。
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上
instanceof原理就是一层一层查找__proto__,如果和构造函数的prototype相等则返回true,如果一直没有查找成功则返回false
instance.[__proto__...] === instance.constructor.prototype
模拟实现instanceof
function instance_of(L, R) { // L 表示左边表达式 R表示右边表达式
const O = R.prototype; // 取R的显式原型
L = Object.getPrototypeOf(L); // 取L的隐式原型 L = L.__proto__
while(true) {
// Object.prototype.__proto__ === null
if(L===null) {
return false;
}
if(L===O) { // 当L严格等于O时,返回true
return true;
}
L = Object.getPrototypeOf(L); // // 没找到继续向上一层原型链查找 L = L.__proto__
}
}
function Foo(name) {
this.name = name;
}
var f = new Foo('nick')
f instanceof Foo // true
f instanceof Object // true