blog icon indicating copy to clipboard operation
blog copied to clipboard

【进阶5-1期】重新认识构造函数、原型和原型链

Open yygmind opened this issue 5 years ago • 14 comments

  • 引言
  • 构造函数
    • 什么是构造函数
    • Symbol 是构造函数吗
    • constructor 值只读吗
    • 模拟实现 new
  • 原型
    • prototype
    • __proto__
    • 注意点
    • 优化实现 new
  • 原型链
  • 小结
  • 参考
  • 进阶系列目录
  • 交流

引言

前端进阶系列已经到第 5 期啦,本期正式开始原型 Prototype 系列。

本篇文章重点介绍构造函数、原型和原型链相关知识,如果你还不知道 Symbol 是不是构造函数、constructor 属性是否只读、prototype[[Prototype]]__proto__ 的区别、什么是原型链,建议你好好阅读本文,希望对你有所帮助。

下图是本文的思维导图,高清思维导图和更多文章请看我的 Github

1111

构造函数

什么是构造函数

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

// 木易杨
function Parent(age) {
    this.age = age;
}

var p = new Parent(50);
p.constructor === Parent; // true
p.constructor === Object; // false

构造函数本身就是一个函数,与普通函数没有任何区别,不过为了规范一般将其首字母大写。构造函数和普通函数的区别在于,使用 new 生成实例的函数就是构造函数,直接调用的就是普通函数。

那是不是意味着普通函数创建的实例没有 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

Symbol 是构造函数吗

MDN 是这样介绍 Symbol

The Symbol() function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()".

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

// 木易杨
new Symbol(123); // Symbol is not a constructor 

Symbol(123); // Symbol(123)

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

// 木易杨
var sym = Symbol(123); 
console.log( sym );
// Symbol(123)

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

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

constructor 值只读吗

这个得分情况,对于引用类型来说 constructor 属性值是可以修改的,但是对于基本类型来说是只读的。

引用类型情况其值可修改这个很好理解,比如原型链继承方案中,就需要对 constructor重新赋值进行修正。

// 木易杨
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);

image-20190210152306297

对于基本类型来说是只读的,比如 1、“muyiy”、true、Symbol,当然 nullundefined 是没有 constructor 属性的。

// 木易杨
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)

为什么呢?因为创建他们的是只读的原生构造函数(native constructors),这个例子也说明了依赖一个对象的 constructor 属性并不安全。

模拟实现 new

说到这里就要聊聊 new 的实现了,实现代码如下。

// 木易杨
function create() {
	// 1、创建一个空的对象
    var obj = new Object(),
	// 2、获得构造函数,同时删除 arguments 中第一个参数
    Con = [].shift.call(arguments);
	// 3、链接到原型,obj 可以访问构造函数原型中的属性
    Object.setPrototypeOf(obj, Con.prototype);
	// 4、绑定 this 实现继承,obj 可以访问到构造函数中的属性
    var ret = Con.apply(obj, arguments);
	// 5、优先返回构造函数返回的对象
	return ret instanceof Object ? ret : obj;
};

之前写过一篇文章解析 new 的模拟实现过程,如果你对实现过程还不了解的话点击阅读。「【进阶3-5期】深度解析 new 原理及模拟实现

原型

prototype

JavaScript 是一种基于原型的语言 (prototype-based language),这个和 Java 等基于类的语言不一样。

每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。

image-20190210223838636

从上面这张图可以发现,Parent 对象有一个原型对象 Parent.prototype,其上有两个属性,分别是 constructor__proto__,其中 __proto__ 已被弃用。

构造函数 Parent 有一个指向原型的指针,原型 Parent.prototype 有一个指向构造函数的指针 Parent.prototype.constructor,如上图所示,其实就是一个循环引用。

image-20190211154751602

__proto__

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

__proto__ 发音 dunder proto,最先被 Firefox使用,后来在 ES6 被列为 Javascript 的标准内建属性。

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

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

image-20190211194108633

这里用 p.__proto__ 获取对象的原型,__proto__ 是每个实例上都有的属性,prototype 是构造函数的属性,这两个并不一样,但 p.__proto__ Parent.prototype 指向同一个对象。

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

所以构造函数 ParentParent.prototypep 的关系如下图。

image-20190211200314401

注意点

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

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

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

// 木易杨
// 获取
Object.getPrototypeOf()
Reflect.getPrototypeOf()

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

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

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

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

优化实现 new

正如上面介绍的不建议使用 __proto__,所以我们使用 Object.create() 来模拟实现,优化后的代码如下。

// 木易杨
function create() {
	// 1、获得构造函数,同时删除 arguments 中第一个参数
    Con = [].shift.call(arguments);
	// 2、创建一个空的对象并链接到原型,obj 可以访问构造函数原型中的属性
    var obj = Object.create(Con.prototype);
	// 3、绑定 this 实现继承,obj 可以访问到构造函数中的属性
    var ret = Con.apply(obj, arguments);
	// 4、优先返回构造函数返回的对象
	return ret instanceof Object ? ret : obj;
};

原型链

每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。

我们看下面一个例子

// 木易杨
function Parent(age) {
    this.age = age;
}

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

这里 p.constructor 指向 Parent,那是不是意味着 p 实例存在 constructor 属性呢?并不是。

我们打印下 p 值就知道了。

image-20190219214338236

由图可以看到实例对象 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

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

image-20190213164902615

小结

  • Symbol 作为构造函数来说并不完整,因为不支持语法 new Symbol(),但其原型上拥有 constructor 属性,即 Symbol.prototype.constructor
  • 引用类型 constructor 属性值是可以修改的,但是对于基本类型来说是只读的,当然 nullundefined 没有 constructor 属性。
  • __proto__ 是每个实例上都有的属性,prototype 是构造函数的属性,这两个并不一样,但 p.__proto__ Parent.prototype 指向同一个对象。
  • __proto__ 属性在 ES6 时被标准化,但因为性能问题并不推荐使用,推荐使用 Object.getPrototypeOf()
  • 每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这就是原型链。

参考

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。

https://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

yygmind avatar Feb 19 '19 00:02 yygmind

辛苦了, 继续等你的系列!

wsgouwan avatar Feb 22 '19 01:02 wsgouwan

优质的博文

youling4438 avatar Feb 26 '19 15:02 youling4438

满满的干货,辛苦

denops avatar Mar 08 '19 02:03 denops

辛苦,每天都拜读,期待您的更新

wuquan1995 avatar Mar 08 '19 05:03 wuquan1995

干得漂亮

fengjinlong avatar Mar 23 '19 02:03 fengjinlong

跟着大佬的步伐走

FISHzzj avatar Mar 25 '19 02:03 FISHzzj

干得漂亮

fengjinlong avatar Mar 25 '19 02:03 fengjinlong

看了大佬的文章,然后在看这图,有了更深入的理解,,赞 1671d387e4189ec8

struggle-fish avatar Apr 15 '19 12:04 struggle-fish

基本类型并不存在原型链,对它们做一些操作,比如toString,存在一个boxing和unboxing的过程

StevenX911 avatar Jul 30 '19 12:07 StevenX911

图片全部挂了,建议使用github作为图库

jackchoumine avatar Jul 30 '20 23:07 jackchoumine

构造函数

什么是构造函数

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

// 木易杨
function Parent(age) {
    this.age = age;
}

var p = new Parent(50);
p.constructor === Parent; // true
p.constructor === Object; // false

构造函数本身就是一个函数,与普通函数没有任何区别,不过为了规范一般将其首字母大写。构造函数和普通函数的区别在于,使用 new 生成实例的函数就是构造函数,直接调用的就是普通函数。

那是不是意味着普通函数创建的实例没有 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

Symbol 是构造函数吗

MDN 是这样介绍 Symbol

The Symbol() function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()".

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

// 木易杨
new Symbol(123); // Symbol is not a constructor 

Symbol(123); // Symbol(123)

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

// 木易杨
var sym = Symbol(123); 
console.log( sym );
// Symbol(123)

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

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

constructor 值只读吗

这个得分情况,对于引用类型来说 constructor 属性值是可以修改的,但是对于基本类型来说是只读的。

引用类型情况其值可修改这个很好理解,比如原型链继承方案中,就需要对 constructor重新赋值进行修正。

// 木易杨
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、“muyiy”、true、Symbol,当然 nullundefined 是没有 constructor 属性的。

// 木易杨
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)

为什么呢?因为创建他们的是只读的原生构造函数(native constructors),这个例子也说明了依赖一个对象的 constructor 属性并不安全。

模拟实现 new

说到这里就要聊聊 new 的实现了,实现代码如下。

// 木易杨
function create() {
	// 1、创建一个空的对象
    var obj = new Object(),
	// 2、获得构造函数,同时删除 arguments 中第一个参数
    Con = [].shift.call(arguments);
	// 3、链接到原型,obj 可以访问构造函数原型中的属性
    Object.setPrototypeOf(obj, Con.prototype);
	// 4、绑定 this 实现继承,obj 可以访问到构造函数中的属性
    var ret = Con.apply(obj, arguments);
	// 5、优先返回构造函数返回的对象
	return ret instanceof Object ? ret : obj;
};

之前写过一篇文章解析 new 的模拟实现过程,如果你对实现过程还不了解的话点击阅读。「【进阶3-5期】深度解析 new 原理及模拟实现

原型

prototype

JavaScript 是一种基于原型的语言 (prototype-based language),这个和 Java 等基于类的语言不一样。

每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。

从上面这张图可以发现,Parent 对象有一个原型对象 Parent.prototype,其上有两个属性,分别是 constructor__proto__,其中 __proto__ 已被弃用。

构造函数 Parent 有一个指向原型的指针,原型 Parent.prototype 有一个指向构造函数的指针 Parent.prototype.constructor,如上图所示,其实就是一个循环引用。

__proto__

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

__proto__ 发音 dunder proto,最先被 Firefox使用,后来在 ES6 被列为 Javascript 的标准内建属性。

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

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

这里用 p.__proto__ 获取对象的原型,__proto__ 是每个实例上都有的属性,prototype 是构造函数的属性,这两个并不一样,但 p.__proto__ Parent.prototype 指向同一个对象。

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

所以构造函数 ParentParent.prototypep 的关系如下图。

注意点

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

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

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

// 木易杨
// 获取
Object.getPrototypeOf()
Reflect.getPrototypeOf()

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

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

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

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

优化实现 new

正如上面介绍的不建议使用 __proto__,所以我们使用 Object.create() 来模拟实现,优化后的代码如下。

// 木易杨
function create() {
	// 1、获得构造函数,同时删除 arguments 中第一个参数
    Con = [].shift.call(arguments);
	// 2、创建一个空的对象并链接到原型,obj 可以访问构造函数原型中的属性
    var obj = Object.create(Con.prototype);
	// 3、绑定 this 实现继承,obj 可以访问到构造函数中的属性
    var ret = Con.apply(obj, arguments);
	// 4、优先返回构造函数返回的对象
	return ret instanceof Object ? ret : obj;
};

原型链

每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。

我们看下面一个例子

// 木易杨
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

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

小结

  • Symbol 作为构造函数来说并不完整,因为不支持语法 new Symbol(),但其原型上拥有 constructor 属性,即 Symbol.prototype.constructor
  • 引用类型 constructor 属性值是可以修改的,但是对于基本类型来说是只读的,当然 nullundefined 没有 constructor 属性。
  • __proto__ 是每个实例上都有的属性,prototype 是构造函数的属性,这两个并不一样,但 p.__proto__ Parent.prototype 指向同一个对象。
  • __proto__ 属性在 ES6 时被标准化,但因为性能问题并不推荐使用,推荐使用 Object.getPrototypeOf()
  • 每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这就是原型链。

ghost avatar Nov 28 '20 12:11 ghost

图片呢?

warrenhathaway avatar Jun 20 '23 09:06 warrenhathaway

你的来信我一收到    谢谢哈   ~~~~

yanpei2016 avatar Jun 20 '23 09:06 yanpei2016

存收到,谢谢!

kingforever avatar Jun 20 '23 09:06 kingforever