乐趣区

关于javascript:15分钟精通javascript原型上

最近面试了很多前端同学,发现有不少同学的前端根底很单薄,会应用 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)

应用 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 件事:

  1. 生成一个新对象
  2. 为这个对象设置 prototype
  3. 应用 this 执行构造函数
  4. 返回这个对象

大家当初马上去本人写一个!写不出再来看我的:

function myNew(constructor, args) {const obj = {}
    Object.setPrototypeOf(obj, constructor.prototype)
    constructor.apply(obj, args)
    return obj
}

当然,当初这个 myNew 在生产环境必定是有问题的:

  1. 咱们可能会有多个参数,像这样调用myNew(constructor, 1, 2, 3)
  2. 通常状况下咱们写构造函数是不会写 return 的,然而一些极限状况下,有的人的构造函数会本人 return 一个对象 …
  3. 最初第一二句咱们能够简写下合成一句

所以改写下:

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 借用来这个办法。

这里我还是要持续开展讲一下,我举个例子:

const a = [1, 2, 3]
a.toString() 

大家想一下,为什么 a 这个数组会有一个叫 toString 的办法?

  1. 首先你这样申明式的创立了一个数组 a,其实背地是 javascript 帮你用 new Array(1, 2, 3) 帮你创立的数组 a
  2. 这个 Array 函数其实就是一个构造函数,联合咱们后面讲到的各种常识,能够得出数组 a 的原型__proto__就是 Array.prototype(a.__proto__ === Array.prototype)
  3. 既然数组 a 上没有 toString 这个办法,javascript 就去它的原型 Array.prototype 上找
  4. 嘿,找到了
  5. 如果没找到的话,就会去 Array.prototype 的原型找(a.__proto__.__proto__ === Object.prototype

讲到这里就差不多了,原型,原型链,构造函数,new 我通通给大家讲了一遍,心愿我讲清楚了。对了 es6 不是带来了 class 的写法吗?今天我再跟大家用 class 改写下咱们的 User 构造函数,还有 extend 继承等概念都会相继讲到,大家期待下吧。

这篇号称 15 分钟读完的文章,花了我 3 个小时才写完,感觉对本人有用的话,记得珍藏点赞哦,另外深圳阿里继续招人,欢送私信勾结

退出移动版