共计 3714 个字符,预计需要花费 10 分钟才能阅读完成。
已经认为遥遥无期的 2022 年,转瞬就到了。自大学本科毕业以来,从事前端开发行业也有五年了,对于日常工作中的业务需要开发根本都已熟能生巧,但总感觉本人还存在很大的晋升空间,兴许是遇到大家口中常说的每隔三年五年就会面临的回升瓶颈了吧。
一个优良的前端工程师,不仅能高效实现页面的开发,还能把握和实际一系列前端工程化的技术,包含脚手架与我的项目脚本,测试体系、监控体系、我的项目标准、我的项目构建和打包、我的项目部署和运维等。不仅能做我的项目,而且有足够的教训和计划做好我的项目,具体能够是性能优化方面或者是技术方面,性能优化如长列表优化、加载性能优化、晋升我的项目的可维护性等,技术方面如微前端、服务端渲染、跨端开发等。
当感到迷茫的时候,须要做的是及时调整好本人的心态,捋清思路,回过头来反思和积淀一下本人的过来,以让本人的能力能失去进一步的晋升。
过来在上下班碎片化工夫浏览一些公众号推文的时候,常常感觉本人的基础知识不过关,把握的前端开发常识未成体系,当初就趁着过年前没那么忙温习一下,打算接下来写一个深刻了解 JavaScript 的系列文章,这篇博客是系列的第一篇,从 JavaScript 的原型与原型链说起。
JavaScript 的面向对象
学习过 Java 的同学应该都晓得,面向对象的语言有三大个性:封装性、继承性、多态性。然而 JavaScript 不是严格意义上的面向对象编程语言,它是一门基于原型的语言,通过原型能够实现继承的个性。
举个例子:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.language = 'chinese'
Person.prototype.sayName = function() {console.log('My name is' + this.name)
}
Person.prototype.sayAge = function() {console.log('My age is' + this.age)
}
Person.prototype.getLanguage = function() {console.log(this.language)
}
let foo = new Person('foo', 25)
console.log(foo.sayName()) // My name is foo
console.log(foo.sayAge()) // My age is 25
console.log(foo.getLanguage()) // chinese
从这个例子咱们能够看到,构造函数 Person 中没有 sayName 办法和 sayAge 办法,然而 foo 实例对象却胜利调用了这两个办法,那为什么呢?这是因为它从原型对象中继承了这两个办法。通过这个例子咱们也引出明天要探讨的话题,JavaScript 中的原型、实例与构造函数三者之间有什么外在的分割呢?
原型 prototype
每个函数都有一个 prototype 属性,它指向通过这个函数创立的实例对象的原型,实例对象都会从这个原型上继承属性,也就是说原型上的属性和办法会被所有实例对象共用。那什么是原型呢?原型能够了解为实例对象在被创立的时候,就会有一个与之相关联的对象,这对象就是咱们常常说的原型。
构造函数与原型的关系能够应用下图来示意:
那实例跟原型是怎么分割起来的呢?
__proto__属性
每个实例对象都有 __proto__公有属性,它指向了构造函数的原型对象,也就是说实例对象是通过 __proto__属性与原型分割起来的。咱们再欠缺一下实例、构造函数、原型之间的关系图:
须要留神的是,__proto__属性素来没有被包含在 ECMAScript 的语言标准中,然而所有古代浏览器都实现了它。__proto__属性已在 ECMAScript6 语言标准中标准化,用于确保 web 浏览器的兼容性,因而它将来也将被反对,但它已不举荐应用,当初更举荐得是应用 Object.getPrototypeOf/Reflect.getPrototypeOf 和 Object.setPrototypeOf/Reflect.setPrototypeOf。
__proto__属性是继承于 Object.prototype 的一个拜访器属性,裸露了一个对象的外部 [[Prototype]]。如果一个对象设置了其余的.__proto__属性,那么将会笼罩原有的结构器原型对象,能够了解为如果扭转了对象的 __proto__属性就会扭转原型链。下文会讲原型链。
上文提到每个实例对象都会有 __proto__属性,应用不同形式创立的对象,它们的__proto__别离指向什么呢?
对象字面量创建对象
let person = {
name: 'tom',
age: 22
}
从控制台输入能够看到,通过对象字面量结构出的对象,其 __proto__指向 Object.prototype,这里也能够晓得 Object 也是一个构造函数。
构造函数创建对象
function Person() {}
let p = new Person()
下面这种模式创建对象的形式就是通过构造函数创建对象,这里的构造函数是 Person 函数。上文也讲过了,通过构造函数创立的对象,其__proto__指向的是构造函数的 prototype 属性指向的对象,即构造函数的原型对象。
Object.create 创建对象
let person = {
name: 'tom',
age: 22
}
const subPerson = Object.create(person);
可见通过 Object.create 创立的对象 subPerson 的 __proto__属性指向了 person,看以下 Object.create 的 polyfill 代码:
if (typeof Object.create !== "function") {Object.create = function (proto, propertiesObject) {if (typeof proto !== 'object' && typeof proto !== 'function') {throw new TypeError('Object prototype may only be an Object:' + proto);
} else if (proto === null) {throw new Error("This browser's implementation of Object.create is a shim and doesn't support'null'as the first argument.");
}
if (typeof propertiesObject !== 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");
function F() {}
F.prototype = proto;
return new F();};
}
通过这段 polyfill 代码不难理解上述例子为什么 subPerson 的 __proto__属性指向了 person。
既然实例对象有__proto__属性指向原型对象,构造函数有 prototype 属性指向原型对象,那么原型对象有没有属性指向实例和构造函数呢?指向构造函数的有,即 constructor,指向实例对象的没有,因为构造函数能够创立很多实例对象,天然没有与原型对象一对一关系的属性,然而每个实例对象都是从原型继承属性和办法。
constructor
因为原型对象能够通过 constructor 属性指向构造函数,事实上这个属性是通过原型链从 Object.prototype 继承而来的。进一步欠缺实例对象、构造函数与原型对象的关系图:
实例对象与原型
从文章结尾的例子,咱们就曾经理解到实例对象会从它的构造函数的原型对象继承属性和办法。如 foo 对象调用 getLanguage 办法的时候,会获取对象的 language 属性并打印进去,然而 foo 对象上并没有设置 language 属性,所以会去与它关联的原型对象上查找,也就是 foo.__proto__ === Person.prototype 对象,这个对象上正好有这个属性,属性值为 “chinese”,于是就被正确打印进去。然而如果实例对象的构造函数的原型上也没有这个属性呢?那么就会去原型的原型对象上查找,直到找到属性为止。
原型链
既然构造函数的原型也是一个对象,那么它是由哪个构造函数创立进去的呢?它的 __proto__属性又指向谁呢?从上文提到对象创立的三种形式中,对象字面量创立的对象由最原始的 Object 构造函数创立,其实构造函数的原型对象也是由它创立的,原型 的__proto__属性天然指向了 Object.prototype,咱们进一步欠缺实例对象、构造函数与原型的关系图:
Object.prototype 也是一个对象,那么它的__proto__属性又指向谁呢?null, 没错,就是 null。
通过 __proto__属性从将 foo 实例对象始终延长到 null 的链状构造就是原型链。