从“有什么用”的角度学前端之ES基础篇(1)

99 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

为什么写这个系列?

工作三年了,回头发现最近一年的成长进步速度实在缓慢,现在的我和一年前的我差别实在有限,许是公司的福利麻痹了我奋斗的心思(哈哈哈哈是真的舒服啊),最近深受在社区认识的大佬刺激,深深感受到自己的不足与懈怠,看到了自己面前的天堑与鸿沟,还是要继续学习呀!

时常翻一翻社区的面试文章也是必须的,有助于了解当下市场关注的技术热点,了解大家在用什么技术什么思路解决什么问题,顺便吐槽一句,最近翻了翻发现...还是和一年前没什么差别。

为什么我建议你看一下?

这类文章已经数不胜数了,但是我们学习枯燥的基础知识及算法时,总免不了思考一个哲学问题:

这玩意儿除了面试还有啥用啊?

so,我希望在接下来的文章中,不仅以理论的角度叙述,更要和你一起思考,这些知识的实用场景,让知识真的转化为生产力,或许,这才是最直接的学习动力。

ES基础

原型&原型链

理论解释:JavaScript常被描述为一种基于原型的语言 (prototype-based language) ——每个对象拥有一个原型对象,原型对象仍然可能有一个它的原型,这样顺着上去,就形成了原型链。

启示:为什么这样设计?人类搞出计算机语言就是为了“省事”,那原型就是一套基本规则,之后根据这套规则创建千千万万个实体,那么我们知道这些实体必然符合原型的规则,那么改了原型,这些实体也就跟着有一样的变化,这就是继承的优点,在实体和原型之间建立了连接,实现了批量应用新功能的办法,原型和继承是一种非常重要的设计思想,它提醒我们,实际开发中N个复杂业务模型,尝试提炼出共同点作为base,然后将base作为一个原型,继承出不同的分支来,这很有利于你面对一个讨厌的情况:给N个业务模型加同样的需求。

如何建立连接?答案是prototype和__proto__。 这两个名词太像了,牢记一点,就能区分了。构造函数身上有prototype,对象身上有__proto__,此处先不要纠结函数也是对象什么的。

按照原型所描述的规则创建一个实体,用到的工具就是构造函数,如果你要从一个构造函数找到原型,那么就使用prototype,如果你要从已经造出来的实体对象找到原型,那么就使用__proto__,从对象找到构造函数,是constructor,就这样。

function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
}

const alice = new Person('Alice',20,'female')

console.log(Person.prototype === alice.__proto__)  //true

Person.prototype.getName = function(){
    return this.name;
}

Person.prototype.father = 'Bob';

console.log(alice.getName()); // 'Alice'
console.log(alice.father); // 'Bob
console.log(alice.constructor) // Person函数会被打印出来

这段代码看起来,原型就是构造函数?no,构造函数的prototype才是!这就是很多小白不好理解的一点,因为看起来构造函数就是描述了“规则”啊。

每个对象身上都有这个__proto__属性属性,调用它你就可以找到原型,实际上JS也是这么做的,原型上的方法和属性并非被复制到实体对象中,而是当你调用alice对象上的getName方法时,JavaScript没找到它身上的getName方法,那么会顺着原型链(不断调用__proto__属性)往上逐级找。这样是不还挺省空间的,算一种时间换空间的做法(我猜的,未考证)。

了解了这些,你应该会有个一理解,在Javascript中,万物皆对象,这很像物理学中在探索的统一场论,用一种唯一的抽象理论,描述世间万物。那么,字符串/数字/布尔值,都可以抽象为字符串对象/数字对象/布尔值对象,因为他们都符合一个特征,有原型对象,都是被特定的构造函数构造出来的,故而它们顺着原型链都会找到Object。尝试寻找统一特征,抽象基本理论,是一个重要的思想。这里做了一个限定,Object再往上是null,到达了原型链的终点,很哲学,万物归一归于虚无。

对开发者来说,原型提供了一种“从根本上解决问题”的办法,例如Vue2的响应式设计,出于性能考虑并没有对数组类型所有的item做拦截,而是大胆的修改了Array构造函数的pop/push等方法(不细讲了),这会影响到所有数组,一种釜底抽薪的艺术。

this

理论解释:this是当前执行上下文(global、function 或 eval)的一个属性,严格模式下可能被赋予任意值。 你完全可以将this理解为你中小学时期学到的意思——“这”,它是一个代词,一个指针,一个箭头。它是当前执行的上下文的代名词,浏览器中,全局执行环境中指向window对象,函数中指向当前上下文,如果不是构造函数或class的方法,普通函数里这个通常没什么意义,因为它是undefined。在构造函数(class一样)中,this指向将来new出来的实体对象,当然这是默认值,你早已听过三大改变this指向的方法:call,apply,bind。

举个栗子,A对象身上有个方法x,你想临时给B对象用一下,那么方法x中的this必然是指向A对象,这时候你就要临时call一下了,让这里的this在本次运行期间指向B对象。区别是,call和apply会立即执行,call将所有参数散列传入,apply接受一个包含所有参数的数组,bind仅仅是改变了this指向,并未执行,需要时再像普通函数一样调用就ok

function Person({firstName,lastName}){
    this.firstName = firstName
    this.lastName = lastName
}
Person.prototype.getNameLength = function(keyName){
    return this[keyName].length
}

function Dog({nickName}){
    this.nickName = nickName
}

const harry = new Person({firstName='Harry',lastName='Potter'})

const goudan = new Dog({nickName:'goudan'})

console.log(harry.getNameLength('lastName')) // 5

console.log(harry.getNameLength.call(goudan,'nickName')) // 6

但ES6新的箭头函数语法中,this并不会如你预期的指向将来的调用者,而是在函数被运行时才会指向某对象,这个对象也不会是函数的调用者,而是指向调用者所处的上下文,你可以理解为“穿透”到父级了。

理解好this,你可以方便的写出面向对象的代码,实例对象身上的方法可以准确的操作实例对象的值,也可以借用别的实例上的方法(前提是合理)。可以说this就是为面向对象而生,面向对象的编程思想旨在让数据的变化可预测,逻辑高内聚,很方便封装出以变量的值为核心的代码。

new的时候发生了什么

初次看到这个题目你可能一惊,这是一个你从没考虑过的问题甚至认为它不需要考虑,天然的就是有个new操作符能按规则造出一个对象来。new的时候主要是四个环节:

  1. 造一个空对象出来,什么都不带,完全的空。
  2. 把这个对象的原型指向某构造函数的prototype,先继承一些祖传属性。
  3. 把构造函数的this指向这个对象并立即执行构造函数,获得一些自身属性。
  4. 给出构造后的对象。

逻辑上三个环节,但代码上只表现为两行核心代码:

function simulateNew() {
    let newObject = null,
        result = null,
        constructor = Array.prototype.shift.call(arguments)   // 参数判断 
    if (typeof constructor !== 'function') {
        console.error('type error')
        return
    }
    // 新建一个空对象,对象的原型为构造函数的 prototype 对象   
    newObject = Object.create(constructor.prototype)  
    // 将 this 指向新建对象,并执行函数
    result = constructor.apply(newObject, arguments)   
    // 判断返回对象
    const flag = result && (typeof result === 'object' || typeof result === 'function')   // 判断返回结果   
    return flag ? result : newObject 
}

实现一个instanceof

趁着原型链相关知识还热乎,我们不难想到判断a对象的__proto__和A构造函数的prototype是否指向同一个原型对象即可,并且顺着原型链不断检查。

    function _instanceof(obj,fn){
      let proto = obj.__proto__;
      while(proto!==null){
        if(proto === fn.prototype){
          return true;
        }
        proto = proto.__proto__;
      }
      return false;
    }

特别的Function

其实万物皆对象这句话,你可能一下就想到了函数也算吗?

按照对象是由构造函数创建的,那函数是什么创建的呢,所有的函数都由一个叫做Function的构造函数创建,这是一个专门用来创建函数的构造函数,那么Function呢,是由Function本身创建的,这里有点鸡生蛋蛋生鸡的意思了。

// 字符串构建函数本身是个函数,函数都由Function创建的,也就是说Function是String的构造函数
String.__proto__ === Function.prototype
// Function 本身也是个函数
Function.__proto__ === Function.prototype
// Object.prototype是任意对象的原型,再往上找原型就到null了
Object.prototype.__proto__ === null
// Function.prototype本身是个对象,对象的构造函数是Object
Function.prototype.__proto__ === Object.prototype

就写到这吧先。。。这篇文章磨蹭了三天才写完,由衷敬佩下那些参与月度更文挑战的前辈们,是全职写文章吗?