【学习】构造函数原型链详解(一):构造函数

一、前言

构造函数、原型、原型链简介。比如我们经常被问到:符号是构造函数吗?构造函数属性是只读的;原型,[[Prototype]] 和 __proto__ 的区别;什么是原型链?以此类推

二、构造函数

1、什么构造函数

构造函数是通过new关键字生成实例的函数。

js的构造函数与其他语言不同,一般规范是首字母大写。

首先,我们来看看这个栗子:

// saucxs
function Parent(age) {
 this.age = age;
}
var p = new Parent(30);
console.log(p); //见下图
console.log(p.constructor); // ƒ Parent(age){this.age = age;}
p.constructor === Parent; // true
p.constructor === Object; // false

这是一个典型的构造函数。构造函数本身也是一个函数,和普通函数没有太大区别。主要区别在于构造函数使用new生成实例,直接调用是普通函数。

2、构造函数属性

返回对创建实例对象的 Object 构造函数的引用。此属性的值是对函数本身的引用,而不是包含函数名称的字符串。

所有对象都从其原型继承构造函数属性:

var o = {};
o.constructor === Object; // true
var o = new Object;
o.constructor === Object; // true
var a = [];
a.constructor === Array; // true
var a = new Array;
a.constructor === Array // true
var n = new Number(3);
n.constructor === Number; // true

普通函数创建的实例有构造函数属性吗?

// saucxs
// 普通函数
function parent2(age) {
 this.age = age;
}
var p2 = parent2(50);
console.log(p2);
// undefined
// 普通函数
function parent3(age) {
 return {
 age: age
 }
}
var p3 = parent3(50);
console.log(p3.constructor); //ƒ Object() { [native code] }
p3.constructor === parent3; // false
p3.constructor === Object; // true

以上代码说明:

(1)普通函数里面有return操作的有constructor属性,没有return的没有constructor属性;

(2)具有constructor属性的普通函数的constructor属性的值不是普通函数本身,而是一个Object。

3、符号是构造函数吗?

MDN 是这样介绍 `Symbol` 的

`Symbol()` 函数返回 **symbol** 类型的值,具有公开多个内置对象成员的静态属性,具有公开全局符号注册表的静态方法,并且类似于内置对象object 类,但作为构造函数不完整,因为它不支持语法“`new Symbol()`”。

符号是一种基本数据类型。作为构造函数,它是不完整的,因为不支持语法 new Symbol()。如果要生成实例,可以直接使用 Symbol()。

// saucxs
new Symbol(123); // Symbol is not a constructor
Symbol(123); // Symbol(123)

虽然Symbol是基本数据类型,但是Symbol(1234)的实例可以获取构造函数属性的值。

// saucxs
var sym = Symbol(123); 
console.log( sym ); // Symbol(123)
console.log( sym.constructor ); // ƒ Symbol() { [native code] }
sym.constructor === Symbol; //true
sym.constructor === Object; //false

这里的constructor属性从何而来?其实就是在Symbol原型上,默认是Symbol函数。

4、构造函数值是只读的?

回答:如果是引用类型,可以修改构造函数属性值,如果是基本类型,是只读的。

在引用类型的情况下,修改它很容易理解。比如在原型链继承的方案中,就是对构造函数重赋值的修正。

// saucxs
function Foo() {
 this.value = 42;
}
Foo.prototype = {
 method: function() {}
};
function Bar() {}
// 设置 Bar 的 prototype 属性为 Foo 的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
Bar.prototype.constructor === Object; 
//true
// 修正 Bar.prototype.constructor 为 Bar 本身
Bar.prototype.constructor = Bar;
var test = new Bar() // 创建 Bar 的一个新实例
console.log(test);

只读原始类型,例如:1、“saucxs”、true、Symbol、null、undefined。 null 和 undefined 也没有构造函数属性。

// saucxs
function Type() { };
var	types = [1, "muyiy", 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,muyiy
// function Boolean() { [native code] },false,true
// function Symbol() { [native code] },false,Symbol(123)

为什么会这样?由于它们是由只读的本机构造函数创建的,因此此示例表明依赖对象的构造函数属性是不安全的。

三、原型

3.1 个原型属性

每个对象都有一个原型对象。该对象以它的原型为模板,并集成了原型中的方法和属性。这些属性和方法是对象构造函数的原型属性,而不是对象实例本身。

在上面找到:

1、父对象有一个原型对象Parent.prototypejs判断对象有没有属性,原型对象上有两个属性,分别是:constructor和__proto__,其中__proto__已被弃用。

2、构造函数Parent有一个指向原型构造函数的指针;原型Parent.prototype有一个指向构造函数Parent.prototype.constrcutor的指针,实际上是一个循环引用。

3.2 __proto__ 属性

在上图中可以看到Parent原型(Parent.prototype)上有一个__proto__属性,它是一个访问器属性(即getter函数和setter函数)。作用:通过__proto__可以访问对象的内部[[Prototype]](一个对象或null)

`__proto__` 读作 dunder proto,最初由 Firefox 使用,后来被列为 ES6 中 Javascript 的标准内置属性。

`[[Prototype]]`是对象的内部属性,外部代码无法直接访问。

// saucxs
function Parent(){}; 
var p = new Parent();
console.log(p);
console.log(Parent.prototype);

1、p.__proto__ 获取对象的原型,__proto__ 是每个实例上的一个属性;

2、原型是构造函数的一个属性;

3、p.__proto__ 和 Parent.prototype 指向同一个对象。

// saucxs
function Parent() {}
var p = new Parent();
p.__proto__ === Parent.prototype
// true

所以构造函数Parent、Parent.prototype和p的关系如下图所示:

注 1:`__proto__` 属性仅在 `ES6` 中标准化

为了确保网络浏览器的兼容性,但出于标准化原因以外的性能问题,不建议使用。为了更好的支持,推荐使用`Object.getPrototypeOf()`。

如果要读取或修改对象的`[[Prototype]]`属性,推荐使用如下方案js判断对象有没有属性,但是此时设置对象的`[[Prototype]]`仍然是一个缓慢的操作,如果性能有问题,就要避免这个操作。

如果你想在继承另一个对象的`[[Prototype]]`的同时创建一个新对象,推荐使用`Object.create()`。

// saucxs
function Parent() {
 age: 50
};
var p = new Parent();
var child = Object.create(p);

这里的 `child` 是一个新的空对象,带有指向对象 p 的指针 `__proto__`。

四、原型链

每个对象都有一个原型对象,它通过__proto__指针指向上一个原型,并从中继承方法和属性。同时,原型对象也可能有原型,最终会指向null。这种关系就变成了原型链(prototype chain),作用:通过原型链,一个对象将拥有其他对象定义的属性和方法。

// saucxs
function Parent(age) {
 this.age = age;
}
var p = new Parent(50);
p.constructor === Parent; // true

p.constructor指向Parent,那么是不是说p实例化中有constructor属性呢?不存在,打印p:

从图中可以看出,实例化的对象p本身并没有构造函数属性。它通过原型链向上查找__proto__,最后找到构造函数属性,该属性指向Parent

// saucxs
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

下图展示了原型链的工作机制。

五、总结

1、符号是一种基本数据类型。作为构造函数不完整,因为不支持语法new Symbol(),但是原型有constructor属性,即Symbol.prototype.constructor。

2、引用类型构造函数属性的值可以修改,但是对于基本类型是只读的。当然,null 和 undefined 没有构造函数属性。

3、__proto__ 是每个实例的属性,prototype 是构造函数的属性。这两个是不同的,但是 p.__proto__ 和 Parent.prototype 指向同一个对象。

4、__proto__ 属性在 ES6 中是标准化的,但由于性能问题不推荐使用。推荐使用 Object.getPropertyOf()。

5、每个对象都有一个原型对象,它通过__ptoto_指针指向上一个原型,并从中继承方法和属性。同时,原型对象也可能有原型。这样一层完成,最终指向null,这就是原型链。

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论