最近面试了很多前端同学,发现有不少同学的前端基础很薄弱,会使用react/vue等库或框架,但对一些前端最核心的概念,如原型,继承,作用域,事件循环等却掌握的模棱两可。所以很多时候你问深入点的问题,或者涉及到原理时,就支支吾吾答不出来。
所以呢,打算更新一个新的系列,专门讲前端的核心基础知识,让大家不管是在前端技术的成长路上,还是面试过程中都能乘风破浪!
今天我们讲javascript里最核心的一个概念:原型。其他文章会陆陆续续更新。
虽然今天我们是要讲javascript的原型,但为了让大家知道为啥要设计这么个东西,我打算从如何生成一个对象讲起。
生成一个简单的对象
最简单的生成对象的方法:
let user = {}
user.name = 'zac'
user.age = 28
user.grow = function(years) {
this.age += years
console.log(`${this.name} is now ${this.age}`)
}
这样生成一个user对象是很简单,假如需要生成一堆user对象该怎么办呢?我们可以创建一个函数专门来生成user:
function User(name, age) {
let user = {}
user.name = name
user.age = age
user.grow = function(years) {
this.age += years
console.log(`${this.name} is now ${this.age}`)
}
return user
}
const zac = User('zac', 28)
const ivan = User('ivan', 28)
这个函数其实有个专门的名字叫工厂函数(Factory Function)
使用Object.create创建对象
但现在我们这个函数有个问题,每一次我们实例化一个User时,就得重新分配内存创建一遍grow方法,怎么优化呢?我们可以把User对象里的方法都移出去:
const userMethods = {
grow(years) {
this.age += years
console.log(`${this.name} is now ${this.age}`)
}
}
function User(name, age) {
let user = {}
user.name = name
user.age = age
user.grow = userMethods.grow
return user
}
const zac = User('zac', 28)
const ivan = User('ivan', 28)
移出去后又遇到一个麻烦的问题,假如我们需要给User新增一个方法,比如sing,
const userMethods = {
grow(years) {
this.age += years
console.log(`${this.name} is now ${this.age}`)
},
sing(song) {
console.log(`${this.name} is now singing ${song}`)
}
}
这时候我们还需要去User里去增加相应的方法:
function User(name, age) {
let user = {}
user.name = name
user.age = age
user.grow = userMethods.grow
user.sing = userMethods.sing
return user
}
这就给后期的维护带来里无穷的麻烦,有没有什么办法可以让我们避免呢?现在我们的User函数每次都是先去生成一个空对象{},我们是不是可以直接用userMethods这个对象为蓝图来生成一个对象呢?这样就可以直接使用userMethods里面的方法了。
javascript为我们提供了这个方法:Object.create(proto)
,这个方法生成一个空对象,并将参数proto设置为自己的原型[[Prototype]]
。原型有什么用呢?简单来说,假如我们在一个对象里找某个属性或方法,没找到,那javascript引擎就会继续往这个对象的原型里找,再找不到就继续往这个对象原型的原型里找,直到找到或者遇到null,这个过程就是原型链啦。ok,我们再来改写User:
function User(name, age) {
let user = Object.create(userMethods)
user.name = name
user.age = age
return user
}
不知道你们有没有注意到,我的User函数首字母是大写的,这样的函数在javascript里叫什么呢?构造函数,也就是conscrutor
,它就是专门用来构造对象的!
借助函数的prototype属性
现在还有一个问题,我们这User构造函数,还得配合着userMethods使用,看上去就很麻烦,javascript里有没有什么方法可以让我们省去写这个userMethods对象呢?
有的!下面我要讲一个很重要的概念————什么是原型prototype
?敲黑板了!javascript里创建的每个函数都带有prototype这个属性,它指向一个对象(这个对象里包含一个constructor属性指向原来的这个函数)
看起来好像很绕口,其实很好理解,我们看个例子,我们创建里一个叫a的函数,它天然包含里prototype属性,打印出来可以看出它是一个对象,这个对象里天然有一个属性叫constructor,它指向的f函数就是我们的a函数本身。
function a() {}
console.log(a.prototype) // {constructor: ƒ}
这里我顺带要讲一个我们刚刚的Object.create(proto)
,我不是也提到了原型[[Prototype]]
吗?敲黑板了!这里千万要注意,如下所示,对象的原型可以通过Object.getPrototypeOf(obj)
或者远古写法__proto__
取到;而函数的本身有一个叫做原型prototype的属性,它是可以直接在函数上找到的f.prototype
。这两者并不是同一个东西。
const b = {}
const c = Object.create(b)
console.log(Object.getPrototypeOf(c) === b) //true
console.log(c.__proto__ === b) // true
好,现在我们在扯回原来的话题,已知每个函数都自带prototype属性,我们是不是可以好好利用这一点,我们根本不需要把user对象需要公用的方法放在userMethods里了,直接放在User函数的prototype里就好啊喂!
function User(name, age) {
let user = Object.create(User.prototype)
user.name = name
user.age = age
return user
}
User.prototype.grow = function(years) {
this.age += years
console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
console.log(`${this.name} is now singing ${song}`)
}
const zac = User('zac', 28)
使用构造函数来生成对象
我的天,简直简洁优雅大方!如此简洁优雅大方以至于javascript决定把这个融入到javascript语言当中去,于是就正式产生了构造函数constructor
,专门用来构造对象的,使用方法就是在构造函数前使用new
指令。我们看下如果直接用javascript的构造函数怎么写:
function UserWithNew(name, age) {
// let this = Object.create(User.prototype)
this.name = name
this.age = age
// return this
}
User.prototype.grow = function(years) {
this.age += years
console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
console.log(`${this.name} is now singing ${song}`)
}
const zac = new UserWithNew('zac', 28)
对比上面我们自己写的User函数,是不是发现并没有太大的差别?那些差别其实就是new
指令做的事情!
new指令到底做了什么
这里我们再稍微拓展下,假如有人要你手写一个new
指令,是不是手到擒来了?总结起来,就是4件事:
- 生成一个新对象
- 为这个对象设置prototype
- 使用this执行构造函数
- 返回这个对象
大家现在马上去自己写一个!写不出再来看我的:
function myNew(constructor, args) {
const obj = {}
Object.setPrototypeOf(obj, constructor.prototype)
constructor.apply(obj, args)
return obj
}
当然,现在这个myNew在生产环境肯定是有问题的:
- 我们可能会有多个参数,像这样调用
myNew(constructor, 1, 2, 3)
- 通常情况下我们写构造函数是不会写return的,但是一些极限情况下,有的人的构造函数会自己return一个对象...
- 第一二句我们可以简写下合成一句
所以改写下:
function myNew(constructor, args) {
const obj = Object.create(constructor.prototype)
const argsArray = Array.prototype.slice.apply(arguments)
const result = constructor.apply(obj, argsArray.slice(1))
if(typeof result === 'object' && result !== null) {
return result
}
return obj
}
注意这里第二句,由于arguments是一个类数组的东西,它本身其实并没有slice这个方法,所以我们向Array.prototype借用来这个方法。第一次使用slice方法作用是将arguments转化为一个数组,第二次使用slice则是为了得到一个去掉第一个元素的新数组。
这里我还是要继续展开讲一下,我举个例子:
const a = [1, 2, 3]
a.toString()
大家想一下,为什么a这个数组会有一个叫toString的方法?
- 首先你这样声明式的创建了一个数组a,其实背后是javascript帮你用
new Array(1, 2, 3)
帮你创建的数组a - 这个Array函数其实就是一个构造函数,结合我们前面讲到的各种知识,可以得出数组a的原型__proto__就是Array.prototype(
a.__proto__ === Array.prototype
) - 既然数组a上没有toString这个方法,javascript就去它的原型Array.prototype上找
- 嘿,找到了
- 假如没找到的话,就会去Array.prototype的原型找(
a.__proto__.__proto__ === Object.prototype
)
最后我们再利用es6的rest语法(不懂的去看我的es6系列文章)改写下myNew函数:
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype)
const result = constructor.apply(obj, args)
if(typeof result === 'object' && result !== null) {
return result
}
return obj
}
构造函数vs工厂函数
结束前,我得稍微提下这个,前面我们先介绍了工厂函数,然后说它不好,然后才介绍了构造函数。但事实上,他们两个谁优谁劣还是得辩证的来看。
工厂函数跟构造函数比起来至少有两个好处:
- 工厂函数可以有私有变量和方法
- 工厂函数没有this,不会遇到this带来的bugs
但这两个好处慢慢的也不明显了
- 首先javascript的提案为class设计了私有变量,变量前加上
#
,如下,waterLimit这个变量只有class内部才能访问,外部不可以访问。不过这个提案现在还没完全落实。 - es6的箭头函数也在绝大多数情况下解决了this的问题。
class CoffeeMachine {
#waterLimit = 200;
...
总结
讲到这里就差不多了,原型,原型链,构造函数,new我统统给大家讲了一遍,希望我讲清楚了。对了es6不是带来了class的写法吗?明天我再跟大家用class改写下我们的User构造函数,还有extend继承等概念都会相继讲到,大家期待下吧。
一次性精通javascript原型/继承/构造函数/类的原理(下)
花了3个小时才写完,觉得对自己有用的话,记得收藏点赞哦,另外深圳阿里持续招人,欢迎私信勾搭
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。