Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yygmind opened this issue Feb 19, 2019 · 14 comments
Open

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

yygmind opened this issue Feb 19, 2019 · 14 comments

Comments

@yygmind
Copy link
Owner

yygmind commented Feb 19, 2019

引言

前端进阶系列已经到第 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

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

@wsgouwan
Copy link

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

@youling4438
Copy link

优质的博文

@denops
Copy link

denops commented Mar 8, 2019

满满的干货,辛苦

@wuquan1995
Copy link

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

@fengjinlong
Copy link

干得漂亮

@FISHzzj
Copy link

FISHzzj commented Mar 25, 2019

跟着大佬的步伐走

@fengjinlong
Copy link

干得漂亮

@struggle-fish
Copy link

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

@StevenX911
Copy link

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

@jackchoumine
Copy link

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

@ghost
Copy link

ghost commented Nov 28, 2020

构造函数

什么是构造函数

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,这就是原型链。

@warrenhathaway
Copy link

图片呢?

@yanpei2016
Copy link

yanpei2016 commented Jun 20, 2023 via email

@kingforever
Copy link

kingforever commented Jun 20, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests