阴影中的曙光

你不知道的JS

2019.10.10 / JavaScript / 点击 599 / 回复 0 / JavaScript

没办法啊前端基本上就是看JS能力

作用域

变量出现在赋值操作符左侧时进行LHS查询 e.g. data = 123 寻找物理存储的地址
变量出现在赋值操作符右侧时进行RHS查询 e.g. var some = data 寻找物理存储内的数据

  • 函数逻辑操作应该遵从尽量少引用外部变量,尽量降低函数粒度,尽量减少副作用等原则
  • 以var声明变量的时候实际上是会提升到函数的最顶端对所有变量进行声明,如果声明的同时进行赋值,则会在相应行数再进行变量的赋值.
  • 函数和变量的声明都会提升,但是如果函数和变量占用了同一个变量名,则函数会优先声明.
  • 函数可以在定义时的词法作用域外的地方调用,并且此函数能访问到定义时的词法作用域,就是闭包.因为这个索引函数始终保持这对其定义时词法作用域变量的访问权限,所以垃圾回收机制无法对这个作用域内的变量进行回收,从而形成’内存泄漏’
  • JS作用域遵从词法作用域,及关注函数定义时的位置,而非动态作用域语言中寻找变量参照调用顺序,延调用栈寻找变量

This

This的绑定主要取决于函数的调用栈以及调用位置,调用位置的判断遵循以下四个原则

  • 默认绑定 ,函数在全局作用域调用时,无显示调用关系,则this指向全局对象
  • 隐式绑定,独立声明的函数在以 对象.函数名() 的形式调用时函数内this指向此对象,但是值得注意的是
    function foo() {
        console.log(this.a);    
    }
    var obj = {
        a: 2,
        foo:foo
    };
    var bar = obj.foo;
    var a = "oops,global"
    bar(); //"oops,global"

在 对象.函数名 这种组合做右值的时候其实本质上只是完成了一个函数本身的引用,并不能改变函数内this指向,所以会采用默认绑定,函数传参的本质也是将实参作为右值赋给形参,所以 setTimeout( obj.foo, 100 ) 这类的写法也都是遵从默认绑定规则而不是隐式绑定

  • 显示绑定, call方法和apple方法
  • New 绑定
  • 参数预设
function foo(p1,p2) {
      this.val = p1+p2
}
var bar = foo.bind(null , "p1")
var baz = new bar("p2")
baz.val //p1p2

函数

函数调用的几种方式

  • 直接调用 fun();

     非严格模式this指向宿主环境,严格模式下undefined
  • 方法调用 object.fun();

     this指向调用此方法的宿主对象
  • 构造函数 new fun();
    新建一个对象,然后将其作为this的绑定传递给构造函数,最后返回此函数,构造函数的返回值如果为object,则通过new调用时就会返回此对象,否则返回自动创建的对象.

如果构造函数不通过new调用,则正常返回return的值.不推荐这么做,直接调用构造函数时this的指向往往是undefined或者Windows,详见直接调用function
及如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的this将被丢弃。但是,如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象。

  • 代理调用 fun1.call(fun2)

JS中的对象

JS并不像Java,C++等面向对象语言通过类来进行对象描述,而是采用原型的方式,而JS语言的作者又因为早期的公司政治原因不得不去向java靠拢,所以在原型的基础上又引入了new this等特性,使其更像java。
JS中对象具有唯一标识性,即便完全相同的两个对象,也不是同一个对象。
对象具有状态,同一对象可以有不同的状态表达。
对象具有行为,对象可能因为其行为产生变迁。
JS的状态和行为在C++中叫成员变量和成员函数,Java中叫属性和方法。JS把状态和行为都抽象成了属性,所以Object内的变量和函数都是他的属性。
JS内的对象具有高度的动态性,对象可以在运行过程中添加更改其状态和行为,JS甚至提供了getter和setter这种访问器属性
对于JS来说属性并未简单的名称和值的键值对,JS的对象有两类属性。

  • 数据属性: 具有四个特征Value(属性的值)Writable(可写性)Enumerable(可枚举性)Configurable(可配置性)
  • 访问器属性:具备Enumerable(可枚举性)Configurable(可配置性)而且具有getter,setter 函数,在变量被读取和赋值时执行
    Js对象在运行时就是一个”属性的集合”,属性以字符串或者symbol 为key,以特征值为value

闭包

一般来讲,绑定了特定执行环境的函数就是一个闭包,闭包相当于一个气泡包含着函数的自身作用域以及其词法环境(函数中使用的的未声明但是通过执行上下文获取到的变量),只要这个闭包函数被引用着,则其包含的所有变量(函数自身变量,外部引用变量)就都会存在,即使外部引用变量所在的作用域已经消失,这也是闭包会占用大量内存空间的原因.
函数获取词法环境内的变量是js的重要机制,但是要注意的是函数只能获取其定义时的词法环境而不能获取其调用时的词法环境

简易状态机代码

var token = [];
const start = char => {
    if(char === '1' 
        || char === '2'
        || char === '3'
        || char === '4'
        || char === '5'
        || char === '6'
        || char === '7'
        || char === '8'
        || char === '9'
        || char === '0'
    ) {
        token.push(char);
        return inNumber;   
    }
    if(char === '+' 
        || char === '-'
        || char === '*'
        || char === '/'
    ) {
        emmitToken(char, char);
        return start
    }
    if(char === ' ') {
        return start;
    }
    if(char === '\r' 
        || char === '\n'
    ) {
        return start;
    }
}
const inNumber = char => {
    if(char === '1' 
        || char === '2'
        || char === '3'
        || char === '4'
        || char === '5'
        || char === '6'
        || char === '7'
        || char === '8'
        || char === '9'
        || char === '0'
    ) {
        token.push(char);
        return inNumber;
    } else {
        emmitToken("Number", token.join(""),char);
        token = [];
        return start(char); // put back char
    }
}

function emmitToken(type, value) {
  console.log(value);
}

var input = "1024 + 2 * 256"

var state = start;

for(var c of input.split('')){
  state = state(c);
}

state(Symbol('EOF'))


output>>>>>
1024
+
2
*
256

no LineTerminator here

JavaScript会自动对代码进行插入 ; 的操作,其中很重要的插入规则就是:
有换行符,且语法中规定此处不能有换行符,就自动插入分号
而这里的不能有换行符就是no LineTerminator here,具体的一般有这下情况

  • 带continue或者break的语句,不能在其后边接换行
  • return后不能接换行
  • 后自增,后自减运算符前不能插入换行
  • async关键字后不能插入换行
  • 箭头函数的箭头前不能插入换行
  • yield后不能插入换行
    在此类不可插入换行符的情况下,js会自动补入分号,以达到不接换行符的目的,除这些情况,js引擎则均不会自动补入分号