Fork me on GitHub

重新认识构造函数、原型和原型链

一、构造函数

啥是构造函数?

构造函数本身就是一个函数,与普通函数没有区别,处于规范的目的首字母大写。构造函数与普通函数的区别是构造函数使用new生成实例,普通函数则直接调用。

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

1
2
3
4
5
6
7
8
9
10
function Dog (color) {
this.color = color;
}
let d = new Dog('white');

d.constructor === Dog; // true
d.constructor === Object; // false

Dog.constructor === Function; // true
Dog.constructor === Object; // false

普通函数创建的实例一定没有constructor属性?NO!

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function dog (color) {
this.color = color;
}

let d = dog('white'); // undefined

function boy(age) {
return {
age: age
}
}
const b = boy(18);
p3.constructor === Object; // true

Symbol 是构造函数吗?

Symbol 是基本数据类型,但作为构造函数来说它并不完整,因为它不支持语法 new Symbol(),Chrome 认为其不是构造函数,如果要生成实例直接使用 Symbol() 即可。(来自 MDN)

1
2
new Symbol(123); // Symbol is not a constructor 
Symbol(123); // Symbol(123)

虽然是基本数据类型,但 Symbol(123) 实例可以获取 constructor 属性值。

1
2
3
4
5
6
var sym = Symbol(123); 
console.log( sym );
// Symbol(123)

console.log( sym.constructor );
// ƒ Symbol() { [native code] }

这里的 constructor 属性来自哪里?其实是 Symbol 原型上的,即 Symbol.prototype.constructor 返回创建实例原型的函数, 默认为 Symbol 函数。

constructor的值只读么?不一定!

引用类型 constructor 属性值是可以修改的,但是对于基本类型来说是只读的,当然 null 和 undefined 没有 constructor 属性。

  • 引用类型的constructor属性值可以修改这一点原型链继承方案中就可以说明,需要对constructor重新赋值进行修正。
1
2
3
4
5
function Animal(color) {
this.color = color;
}
function Dog() {}
Dog.prototype = new Animal();

new操作符模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
function create () {
// 1.创建一个空的对象
let obj = new Object(),
// 2.获取构造函数,同事删除arguments中的第一个参数
con = [].shift.call(arguments);
// 3.链接到原型,obj可以访问构造函数原型中的属性
Object.setPrototypeOf(obj, con.prototype);
// 4.绑定this实现继承,obj可以访问构造函数中的属性
let ret = con.apply(obj, arguments);
// 5.优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
}

二、原型

javascript是基于原型的语言
每个对象拥有一个原型对象,对象以原型为模版,从原型继承方法和属性,这些属性和方法定义在对象的构造函数的prototype属性上,而非对象实例本身。
例如:

1
2
3
4
5
6
function Parent () {}
console.log(Parent.prototype)
// {
constructor: ƒ Parent()
__proto__: Object
// }

可以看出,Parent对象的原型对象上有两个属性,分别是constructor和 proto proto 已被弃用。

构造函数Parent有一个prototype属性,为指向原型的指针,原型Parent.prototype有一个constructor属性,为指向构造函数的指针,是一个循环引用。

⚠️ proto 属性,这是一个访问器属性(即 getter 函数和 setter 函数),通过它可以访问到对象的内部[[Prototype]] (一个对象或 null )。最先被 Firefox使用,后来在 ES6 被列为 Javascript 的标准内建属性。[[Prototype]] 是对象的一个内部属性,外部代码无法直接访问。

遵循 ECMAScript 标准,someObject.[[Prototype]] 符号用于指向 someObject 的原型。

proto 是每个实例上都有的属性,指向构造函数的原型对象,例如:

1
2
3
4
5
function Parent () {}
const p = new Parent();
p.__proto__ === Parent.prototype // true
Parent.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null

⚠️注意:
proto 属性在 ES6 时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()。

通过改变一个对象的 [[Prototype]] 属性来改变和继承属性会对性能造成非常严重的影响,并且性能消耗的时间也不是简单的花费在 obj.proto = … 语句上, 它还会影响到所有继承自该 [[Prototype]] 的对象,如果你关心性能,你就不应该修改一个对象的 [[Prototype]]。

如果要读取或修改对象的 [[Prototype]] 属性,建议使用如下方案,但是此时设置对象的 [[Prototype]] 依旧是一个缓慢的操作,如果性能是一个问题,就要避免这种操作。

1
2
3
4
5
6
7
// 获取
Object.getPrototypeOf()
Reflect.getPrototypeOf()

// 修改
Object.setPrototypeOf()
Reflect.setPrototypeOf()

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

1
2
3
4
5
6
function Parent () {
height: 180
}

var p = new Parent();
var child = Object.create(p);

这里 child 是一个新的空对象,有一个指向对象 p 的指针 proto

Object.create()实现new

1
2
3
4
5
6
7
8
9
10
function create() {
// 1.获取构造函数并删除arguments中第一个参数
let con = [].shift.call(arguments);
// 2.创建一个空对象并链接到原型,obj可以访问构造函数原型中的属性
let obj = Object.create(con.prototype);
// 3.绑定this,实现继承,obj可以访问构造函数中的属性
let ret = con.call(obj, arguments);
// 4.优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
}

三、原型链

每个对象拥有一个原型对象,通过 proto 指针指向上一个原型,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null,这种关系被称为原型链

例如:

1
2
3
4
5
6
function Parent(age) {
this.age = age;
}

var p = new Parent(10);
p.constructor === Parent // true

p 实例存在 constructor 属性么?其实不是

1
2
p.__proto__ === Parent.prototype // true
Parent.prototype.constructor === Parent // true

实例对象 p 本身没有 constructor 属性,是通过原型链向上查找 proto ,最终查找到 constructor 属性,该属性指向 Parent。

1
2
3
4
5
6
7
8
9
function Parent(age) {
this.age = age;
}
var p = new Parent(10);

p; // Parent {age: 10}
p.__proto__ === Parent.prototype; // true
p.__proto__.__proto__ === Object.prototype; // true
p.__proto__.__proto__.__proto__ === null; // true

原型链运作机制

⚠️注意:原型上的方法和属性被继承到新对象中,并不是复制到新对象,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function Foo(name) {
this.name = name;
}

Foo.prototype.getName = function() {
return this.name;
}

Foo.prototype.length = 3;

let foo = new Foo('test');
console.dir(foo);
// {
// name: 'test',
// __proto__: {
// getName: ƒ ()
// length: 3
// constructor: ƒ Foo(name)
// __proto__: {
// constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
// }
// }
// }

原型上的属性和方法定义在prototype对象上,而非对象实例本身。当访问一个对象的属性/方法时,它不仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层层向上查找,直到找到名字匹配的属性/方法或到达原型链的顶端(null)。

比如调用foo.valueOf()的过程:

  • 首先检查foo对象是否具有可用的valueOf()方法
  • 如果没有,检查foo对象的原型对象,即Foo.prototype是否具有可用的valueof()方法
  • 如果没有,检查Foo.prototype所指对象的原型对象,即Object.prototype是否具有可用的valueOf()方法。找到并调用该方法。

过程图

prototype和 proto

原型对象prototype是构造函数的属性, proto 是每个实例上都有的属性,只有函数才有prototype属性,而每个对象都有 proto 属性来标识自己所继承的原型。这两个是不同的。

1
foo.__proto === Foo.prototype // true

思考:原型链的构建是依赖于prototype还是 proto ?

答:原型链的构建依赖于 proto ,通过foo. proto 指向 Foo.prototype,foo. proto . proto 指向 Bichon.prototype,如此一层一层最终链接到 null。过程如下图:

⚠️注意:不要使用Son.prototype = Parent,这样不会执行Parent的原型,而是指向函数Parent,这样原型链会回溯到Function.prototype而不是Parent.prototype,Parent原型上的方法将不会在Son的原型链上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent() {}

Parent.prototype.eye = function() {
console.log('black');
}

function Son() {}

Son.prototype = Parent

Son.prototype.__proto__ === Function.prototype // true

let s = new Son()
s.eye(); // Uncaught TypeError: s.eye is not a function

instanceof原理及实现

instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

1
2
3
4
5
6
7
function A () {}
function B () {}

var a = new A();

a instanceof A; // true
a instanceof B; // false

原理就是一层一层查找 proto,如果和 constructor.prototype 相等则返回 true,如果一直没有查找成功则返回 false。

1
instance.[__proto__...] === instance.constructor.prototype

模拟实现:

1
2
3
4
5
6
7
8
9
function instanceof(left, right) {
let left = left.__proto__;
let prototype = right.prototype;
while (true) {
if (left == null) return false;
if (left === prototype) return true;
left = left.__proto__;
}
}

⚠️扩展思考题:
有以下 3 个判断数组的方法,对比分析它们之间的区别和优劣:

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

1.Object.prototype.toString.call()

每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。

这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。

1
2
3
4
5
6
7
8
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"
Object.prototype.toString.call([1,2,3]); // "[object Array]"

Object.prototype.toString.call() 常用于判断浏览器内置对象时。

2.instanceof

instanceof的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
比如,使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。

1
2
[]  instanceof Array; // true
[] instanceof Object; // true

但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。

3.Array.isArray()

用于判断对象是否为数组

  • instanceof 与 isArray
    当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes
1
2
3
4
5
6
7
8
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

Array.isArray(arr); // true
Object.prototype.toString.call(arr); // "[object Array]"
arr instanceof Array; // false
  • Array.isArray() 与 Object.prototype.toString.call()
    Array.isArray()是ES5新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。
1
2
3
4
5
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function A() {};
function B() {};

A.prototype = { fun: function() {}};

var a = new A();
console.log(a.constructor === A); // false
// a.constructor === a.__proto__.constructor === A.prototype.constructor === Object

console.log(A.prototype.constructor === A); // false
// A.prototype.constructor === Object

console.log(a.hasOwnProperty('constructor')); // false

console.log(a instanceof A); // true
// a.__proto__ === A.prototype


A.prototype = new B();
var b = new A();

console.log(b.constructor === A); // false
// b.constructor === b.__proto__.constructor === A.prototype.constructor === B

console.log(B.prototype.constructor === A); // false
// B.prototype.constructor === B

console.log(b.constructor.prototype.constructor === A); // false
// b.constructor.prototype.constructor === b.__proto__.constructor.prototype.constructor === A.prototype.constructor.prototype.constructor === B.prototype.constructor === B

console.log(b.hasOwnProperty('constructor')); // false

console.log(b instanceof A); // true
// b.__proto__ === A.protoptype

console.log(b instanceof B); // true
// b.__proto__.__proto__ === B.prototype

参考文章:
https://github.com/yygmind/blog/issues/32
https://juejin.im/post/5ca9cebb6fb9a05e505c5f81
《javascript高级程序设计》第六章

本文标题:重新认识构造函数、原型和原型链

文章作者:tongtong

发布时间:2019年03月19日 - 19:03

最后更新:2019年04月11日 - 14:04

原始链接:https://ilove-coding.github.io/2019/03/19/1-1重新认识构造函数、原型和原型链/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束-------------