了解javaScript秉性和特点,走出死记硬背面试题的禁锢,做一个技术人,将技术成为自己一个属性

778 阅读14分钟

话说在前头(面试捷径说在后头)

最近一直从原理去理解js基础,发现自己收货颇丰。以前一触碰原理性解析的文章或者博客时一直很抵, 但是最近强行去了解到本质就感觉语言设计还是那么的清爽。一些规则和设计也是为了解决问题 高效的解决问题问题。 一门语言也是程序员的工具吧,使用好工具不仅仅只是要看懂说明书(api),应用语言api,一定要懂得这门语言的特点和秉性,这样使用起来就会收放自如,才能发挥它的最大的潜能。

知己知彼,融会贯通,无功胜有功(吹牛逼了)

比如: JS最开始纯解释性语言,js虚拟机的解析器将源码转编译成中间代码,之后直接使用解释器执行中间代码,然后直接输出结果。

js解析的原理都一样,但是也有好几种js虚拟机,他们之间也存在一些差异,苹果公司Safari的javaScriptCore虚拟机,firefox有TraceMonkey虚拟机,而Google则使用v8虚拟机

那V8是怎么执行js代码的呢? v8是混合编译执行和解释执行这两种手段,混合使用编译器和解释器的技术成为JIT(Just In Time)技术

所以我认为js是解释性语言,只是为了优化js运行速度,混合加入了编译器及时编译技术。《你不知道js》说js是门编译语言,有点歧义,不能够更好的说明现代js的核心原理。

MDN中则表达的很准确,感觉MDN才是学标准规范js的最佳地方:

javaScript 是一种具有 函数优先 的轻量级,解释型或即时编译型的编程语言。

这个表达总结多规范,多精美。看着都爽,干货满满

从V8解析javaScript认识该语言特性

1. 特点一:一等公民

js的函数可以向变量一样,拿来赋值,能够作为一个参数传递给另外一个函数,一个函数返回另外一个函数。一等公民设计,就有了闭包的出现,就有了函数式编程等。

当函数内部引用了外部变量时,使用这个函数进行赋值、传参或者作为返回值,你需要保证这些被引用的外部变量确定存在的,这就让函数作为一等公民的麻烦,因为虚拟机还需要处理函数引用的外部变量。

所以当初js创造的时候若要支持函数是一等公民时候,就必须有词法作用域的出现,有了词法作用域和函数一等公民的特性,闭包自然是他们的产物。并不是js为了去创建闭包。

闭包就是函数和它的外部环境的绑定形成的,js的开发者使用这个特性进行简洁的代码编写, 所以第三方代码都是大量的使用,可以让自己的代码更简洁。所以明白闭包,是阅读第三方源码的基础。

2. 特点二: 函数及对象

js是一门基于对象的语言,大部分内容是由对象组成,这些对象还可以动态修改其内容,这就造就了js是一门超级灵活的语言,加大了理解和使用这门语言的难度 在js中函数也是一个对象,函数也有属性和值, 我们声明的函数原型对象是Function,是Function构造函数创建出来的。 还有一个原型继承的主要的公开属性 prototype。 还有两个隐藏属性 name code. v8是靠这两个隐藏属性实现函数可调用的特性, - name:就是函数的名字,如果这个函数没有被设置,name这个函数就是我们常见的匿名函数(anonymous) - code:表示函数代码,以字符串存储在内存中,当执行一个函数调用语句(函数名加括号)时,V8便会从函数对象中取出code属性值,也就是函数代码,然后再解释执行这段函数代码

如果在对象中的属性值是函数,我们就叫这个属性称为方法,所以我们说对象具备属性和方法。

3. 特点三:对象属性分为 排序属性常规属性

对象有两个隐藏属性,element 和 properties 分别指向elements 对象和 properties属性

排序属性(elements): 对象中属性名为数字的属性,根据索引值升序排列 常规属性(properties): 对象中属性名为字符串就被称为常规属性,根据创建时间升序排列

V8,为了有效的提升存储和访问这两种属性的性能,使用了两个线性的数据结构, v8会先从elements属性中按照顺序读所有的元素,然后再在properties读取所有元素,这样就完成了一次索引查询对象属性操作。

只是这样设计的话,在索引properties属性的时候,会先使用隐藏属性properties找到properties对象,再在properties对象中找到这个属性,多了一个properties对象查找的操作。 于是v8除了一个优化策略,将部分常规属性直接存储到对象本身,称为对象内属性。 对象内属性数量是固定的,默认是10个,如果这个对象常规属性多出了10个,那么将剩余的常规属性放在常规属性对象当中。

将常规属性放到对象内属性使用线性结构存储,这些属性也称快属性,查属性非常快,线性结构删除和添加属性特别耗时间和内存开销。 当常规属性特别多的时候,将部分属性放到properties里面存储,这里的存储策略是慢属性策略,非线性数据结构存储(词典)。

4.特点四: 函数表达式 和 立即调用函数表达式(IIFE)

函数表达式 和 函数声明

前面说了函数是一等公民,可以作为赋值, 就像var fun = function() {}.

这里有一个函数声明,js也是允许的但是函数声明会造成一个变量提升问题

func()
function func() {console.lohg('我输出了')}

上面代码能够正常运行,但是

func1()
func1 = function() {console.log('我输出了')}

这代码就会报错

VM130:1 Uncaught TypeError: func1 is not a function at :1:1

为什么呢,这个就要明白,表达式 和 语句,作用域,编译阶段,执行阶段这些概念。

记住:函数表达式本质是表达式,函数声明本质是语句。 js在编译(解析)阶段只会解析语句,不会有任何结果输出。但是会将相关变量放到作用域里面,解析的语句时不会执行。在执行阶段只要作用域有这个变量就可以使用,所以func()函数正常运行了 也就是知道为什么使用函数表达式时候为什么报这个错,因为在编译阶段的时候,foo这个变量还是undefined是一个原生对象而不是函数,所以这行代码执行的时候会这个报错

立即调用函数表达式

这个是借助 js有一个括号操作符() 里面可以放一个表达式 (a=3)如果把a=3替换成一个函数,因为()里面只能放表达式,所以会把函数认为是一个函数表达式,表达式在执行阶段都会返回一个函数对象(在作用域中找到)函数对象加上一个()括号就是函数调用,那这个括号里面的函数就立即被执行了, 这样的表达式我们称为 立即调用函数表达式

因为立即调用函数表达式也是一个表达式,所以V8在编译阶段,并不会为该表达式创建函数对象,这样表达式里面的属性和方法就不会被其他代码访问到,就不会污染js的全局环境, 在ES6之前,js没有私有作用域的概念,没有模块化,会造成你模块的变量可能覆盖别人的变量,所以使用立即调用函数表达式,封装起来,避免了相互之间的变量污染。

5. 特点: 原型继承

语言中继承简单的说,就是一个对象可以访问另外一个对象的属性和方法,js是使用原型继承的策略

js对象有一个隐藏属性 proto 称之为对象的原型,这个属性指向的内存当中另外一个对象,这个对象就是原型对象(prototype) 那个对象可以访问其原型对象的方法和属性

原型继承
看上图,我们只要将对象的隐藏属性__proto__不指向内部给的原型对象,指向你自己声明的实力对象即可完成继承,很简单很明了,没有基于类那些繁文缛节。 原型继承核心就是使用隐藏属性__proto__完成继承,我们实际开发当中不能直接使用这个隐藏属性完成继承: - 这个属性是隐藏属性,不是标准定义的 - 使用这个属性会造成严重的性能问题,要是你把这个属性指向了你自己的声明的对象,V8内部生成做了优化的prototype对象就会以为没有被引用会被丢弃回收。

市面上存在一些继承方案,妈的取了一大堆名词烦的不行,但是不知道本质你会被他们搞晕掉。 1.原型链继承 2.借用构造函数继承,3.组合继承(组合原型链继承和借用构造函数继承)(常用),4.寄生继承 5.寄生组合继承,6.原型式继承

构造函数:属性通过参数进行传递,函数体内,通过this设置属性值。

function DogFactory(type,color){
    this.type = type
    this.color = color
}

然后结合 关键字 new就可以创建对象了 .

var singleDog = new DogFactory()

有个小八卦,为什么使用new 关键字配合一个函数,V8(js虚拟机)就会返回一个对象了?大家可以去自行搜索

new本质,V8做了这些事:

var dog = {}
dog.__proto__ = DogFactory.prototype
DogFactory.call(dog, 'Dog', 'Black')

如图:

NEW 操作

就是创建了一个空对象,再把空对象的原型指向DogFactory函数的公开属性prototype,再使用空对象dog去调用空对象,这样dog就是构造函数的this,执行构造函数就将属性填充操作 ,最终创建了dog对象。 其实这里也完成了一个继承操作,上面我们讲了函数即对象,所以构造函数也是对象,我们生成的对象dog继承了构造函数的属性,算是原型继承吧。

如果根据继承的概念我们可以实现一些继承,

原型链继承:将子的构造函数的prototype指向父实例对象(只要是这个函数作为构造函数去创建对象的时候,函数的prototype就指向这个创建对象的原型)这样也完成了继承

关键代码:Worker.prototype = new Person();

借助构造函数继承:就是在子构造函数去调用父构造函数 使用 call apply都行,改变this属性获取父构造函数的属性,完成继承 关键代码: Person.call(this, name, age);

组合继承:上面代码都会造成性能问题和继承追溯问题(V8引擎分配的原型对象销毁 和 constructor指向混乱) 原型对象上还有一个公用属性 constructor这个属性指向构造函数 构造函数的prototype指向原型对象 使用call可以替换掉构造函数的this 关键代码: js Person.call(this, name, age); Worker.prototype = new Person(); Worker.prototype.constructor = Worker; 原型式继承: js function Empty(){}; Empty.prototype = o; 这个关键还是构造函数的prototype指向发生变化 js var woker={ name: 'jia', age: 18, job: '打杂的' } var mine = Object.create(woker); 原理和上面相同,ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。 寄生继承:就是原型式继承套个壳子

寄生组合式继承:集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式

万变不离奇宗,只要明白函数的公开属性prototype,对象的的隐藏属性__proto__ 构造函数 继承这些概念,继承问题迎刃而解。 面试你还要准备这么多继承的方式么,还要记这些代码么,看到本质,你可以和面试官慢慢讲

上面的继承有很多性能问题(这些问题你们可以去查看),寄生组合式继承算是ES6前继承最优解. 在ES6出现了class关键字,extends 虽然是语法糖,但是也做到了继承方式统一,继承最优解了。也许有些兼容问题,但是有babel呀,怕个毛啊!!!

特性6: 作用域链

jsS是基于词法作用域的,词法作用域就是指: 查找作用域的顺序是按照函数定义时的位置来决定的。

查找变量,其查找顺序都是按照当前函数作用域 --> 全局作用域这个路径来的

函数作用域: 每个函数在执行时都需要查找自己的作用域,当函数里面使用到某个变量或者调用某个函数时,便会优先在该函数作用域中查找相关内容.函数执行完之后,作用域就随之销毁

全局作用域: 全局作用域在V8启动的过程中就被创建了,且一直保存在内存中不会被销毁,直到V8退出

如果在当前函数作用域中没有查找到变量,那么V8就会去全局作用域中去查找,这个查找的线路就叫作用域链

首先当V8启动时,会创建全局作用域,全局作用域中包括this, window等变量,还有一些全局Web API接口。

特性7 类型转换 1 + '2' ? 3 : '12'

在标题的简单表达式中,涉及到了两种不同类型的数据相加。要想理清以上两个问题,要知道类型的概念,js的操作类型的策略

在高级语言中,都会给操作的数据赋予指定的类型,类型可以确认一个值或者一组值的具有特定的意义和目的。

每种计算机语言都定义了自己的类型,还定义了如何操作这些类型,另外还定义了这些类型应该如何相互作用,我们就把这个称为类型系统

V8是严格根据ECMAscript规范来执行操作,有兴趣大家可以去了解一下,通俗的讲,V8有一个ToPrimitve方法,其作用是将a和b转换为原生数据类型 转换流程是:

  • 如果valueOf没有返回原始类型,那么就使用toString方法返回值;
  • 如果两个方法都不返回基本类型值,便会触发一个TypeError的错误;

最后你还是不理解(一定要花时间理解),那就可以很确信一点,V8会通过ToPrimitve方法将对象类型转换为原生类型(拆箱?),最后就是两个原生类型相加,如果一个值得类型时字符串时,则另一个值也需要转换为字符串,然后将另一个值转换成字符串 然后做字符串的连接运算,其他情况都会转化数字类型值,做数字相加。面试去吧,哈哈哈哈。

面试捷径

面试哪什么捷径,好事多磨。一定要懂得语言的秉性,从原理去了解语言的特性和基础策略。 这样才能百战百胜。加油,我是水货,骗流量的,上面都是笔记(他不是笔记,没看完吧,一下就翻过来了)