# JS [常见概念]

# 推荐

# JS是解释性语言

编译型语言在计算机运行代码前,先把代码翻译成计算机可以理解的文件

解释型语言的程序不需要在运行前编译,在运行程序的时候才翻译,专门的解释器负责在每个语句执行的时候解释程序代码。这样解释型语言每执行一次就要翻译一次,效率比较低。

js执行过程分为预编译期和执行期(以代码块为单位,边解释边执行),在预编译期,js解释器会对本代码段内所有的 声明的变量和方法进行处理,将变量和方法提到对应的作用域的最前面,该过程只是对变量进行声明,并不会进行初始化或者赋值(缺省值默认为undefined)

# 函数调用栈 (Callee Stack)

调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

  • 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
  • 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
  • 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。
function greeting() {
   console.log(1)
   sayHi();
   console.log(2)
}
function sayHi() {
   return "Hi!";
}

// 调用 `greeting` 函数
greeting();
console.log(3)
1
2
3
4
5
6
7
8
9
10
11
12

上面的代码会按照如下流程执行

  1. 忽略前面所有函数,直到 greeting() 函数被调用。
  2. 把 greeting() 添加进调用栈列表。
  3. 执行 greeting() 函数体中的所有代码。调用栈列表:
    • greeting
  4. 代码执行到 sayHi() 时,该函数被调用。
  5. 把 sayHi() 添加进调用栈列表。
  6. 执行 sayHi() 函数体中的代码,直到全部执行完毕。调用栈列表:
    • sayHi
    • greeting
  7. 返回来继续执行 greeting() 函数体中 sayHi() 后面的代码。
  8. sayHi执行完毕后,删除调用栈列表中的 sayHi() 函数。调用栈列表:
    • greeting
  9. 当 greeting() 函数体中的代码全部执行完毕,返回到调用 greeting() 的代码行,继续执行剩下的 JS 代码。
  10. 删除调用栈列表中的 greeting() 函数。

一开始,我们得到一个空空如也的调用栈。随后,每当有函数被调用都会自动地添加进调用栈,执行完函数体中的代码后,调用栈又会自动地移除这个函数。最后,我们又得到了一个空空如也的调用栈。

# 全局对象 (GO Global Context)

一个全局对象是一个永远存在于 全局作用域的对象中。

在一个 web 浏览器中, 当脚本创建全局变量时,全局变量作为全局对象的成员被创建。

全局对象的 interface 取决于脚本在其中运行的执行上下文. 例如:

  • 在Web浏览器中,脚本没有专门作为后台任务启动的任何代码都将Window 作为其全局对象。这是Web上绝大多数的JavaScript代码。
  • 在 Worker 中运行的代码将WorkerGlobalScope 对象作为其全局对象。
  • 在Node.js 环境下运行的脚本具有一个称为global 的对象作为其全局对象。

# ES3变量对象 (Variable Object) 和 ES3活动对象 (Activation Object)

变量对象VO是与执⾏上下⽂相关的特殊对象,⽤来存储上下⽂的函数声明,函数形参和变量。

变量对象是 ECMAScript 规范术语。在一个执行上下文中,变量对象才被激活。只有激活的变量对象,其各种属性才能被访问。

在函数执行上下文中,变量对象常常被称为活动对象,两者意思相同。活动对象是在进入函数上下文时被创建,初始化时只包括 Arguments 对象。它通过函数的 arguments 属性访问,arguments 属性值为 Arguments 对象。

# 全局上下文 (GC global execution context) 执行上下文 (EC execution context) 执行上下文栈 (ESC execution context stack)

# 全局上下文

当js代码要开始执行的时候,先遇到全局代码,此时会创建一个【全局上下文】并压入【执行上下文栈】,我们用globalContext表示。当程序结束的时候【全局上下文】才会出栈。

对于全局执行上下文,有三个重要属性:

  • 变量对象 - 初始化是全局对象。全局对象是预定义对象,作为 JavaScript 的全局函数和全局属性的占位符。通过全局对象,可以访问其他所有预定义的对象、函数和属性。
  • 作用域链 -
  • this

# 执行上下文 (opens new window)

当一个函数执行的时候会创建一个【执行上下文】,并压入【执行上下文栈】,当函数执行完毕时,会将该函数【执行上下文】从【执行上下文栈】中弹出。

对于每个执行上下文,都有三个重要属性:

  • 变量对象 - 变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。JavaScript 代码不能直接访问该对象,但是可以访问该对象的成员(如 arguments)。
  • 作用域链 -
  • this
  1. 当进入执行上下文时,不会执行代码,只进行分析。此时变量对象包括:
    • 函数的所有形参(如果是函数上下文)
    • 函数声明
    • 变量声明
        function f(a) {  //声明外部函数
            var b = 1;  //声明局部变量,并赋值1
            function c() {}  //声明内部函数
            var d = function () {};  //声明局部变量,并赋值为匿名函数
            b = 2;  //修改变量b的值为2
        }
        f(3);
    
    1
    2
    3
    4
    5
    6
    7
  2. 在进入函数执行上下文后,【活动对象/变量对象】的结构模拟如下。
AO = {
    arguments : {
        0 : 3,  //实参值
        length : 1  //实参长度
    },
    a : 3,  //实参值
    b : undefined,  //声明局部变量b
    c : function c() {}  //声明函数c,引用function c() {}
    d : undefined  //声明局部变量d
}
1
2
3
4
5
6
7
8
9
10
  1. 执行代码。在代码执行阶段会按顺序执行代码,这时可能会修改变量对象的值。
AO = {
    arguments : {
        0 : 3,  //实参值
        length : 1  //实参长度
    }, 
    a : 3,  //实参值
    b : 1,  //初始化赋值
    c : function () {},  //引用声明的函数c
    d : function () {}  //引用函数表达式"d"
}
1
2
3
4
5
6
7
8
9
10

# 执行上下文栈

存放全局上下文和执行上下文, 栈的最底部存放的是

# 垃圾回收 (GC Garbage Collection)

垃圾收集机制原理:找出那些不再使用的变量,然后释放其占用的内存,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间)周期性的执行这一操作垃圾收集的方式有两种:标记清除法和引用计数法。

  • 标记清除法:当变量进入环境时标记为“进入环境”,当离开环境时,标记为’离开环境‘,此时进行垃圾回收。垃圾回收机制在运行的时候会给存储再内存中的所有变量都加上标记(可以是任何标记方式),然后,它会去掉处在环境中的变量及被环境中的变量引用的变量标记(闭包)。而在此之后剩下的带有标记的变量被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾回收机制到下一个周期运行时,将释放这些变量的内存,回收它们所占用的空间。
  • 引用计数法:声明一个变量,将一个引用类型的值赋值给该变量,则该变量的引用次数就为1,如果该变量又赋值给另一个变量,则该变量引用次数加1,如果包含该变量的引用的变量获取了新的值,则该变量的引用次数减1;如果引用次数为0,则被认为是垃圾进行回收。该方法不太常见,IE9以下的DOM和BOM采用的就是这种垃圾收集方法,该方法在遇到循环引用时会产生内存泄漏,导致已销毁变量的内存空间无法被回收 js会自动执行垃圾回收。可以通过手动赋值为null进行回收。

# ES5词法环境 (Lexical Environment)

词法环境就是描述环境的对象,主要包含两个部分:

  • 环境记录(Environment Record) 记录相应环境中的形参,函数声明,变量声明等

  • 对外部环境的引用(out reference)

https://www.cnblogs.com/yiyi17/p/8630957.html https://blog.csdn.net/dhassa/article/details/70945016

# 变量记录 (Environment record)

# 变量提升及函数提升

变量提升:函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部。

js 代码执行过程分为两个阶段

  1. 词法分析:词法分析主要包括:分析变量声明、分析函数声明、分析形参三个部分。此过程会做变量提升及函数提升
  2. 执行阶段

同一个标识符的情况下,变量声明与函数声明都会提升;函数声明会覆盖变量声明,但不会覆盖变量赋值,即:如果声明变量的同时初始化或赋值那么变量优先级高于函数。 所以下列代码会报错。

var a = 10
function a() {
  console.log(1000)
}
a() // 
1
2
3
4
5

# 作用域

作用域(Scope)表示变量的作用范围、可见区域,包括词法作用域和执行作用域。

js采用词法作用域。JavaScript的变量和函数作用域是在定义时决定的,而不是执行时决定的 。由于词法作用域取决于源代码结构,所以 JavaScript解释器只需要通过静态分析就能确定每个变量、函数的作用域,这种作用域也称为静态作用域

  • 词法作用域:根据代码的结构关系来确定作用域。词法作用域是一种静态的词法结构,JavaScript 解析器主要根据词法结构确定每个变量的可见性和有效区域。
  • 执行作用域:当代码被执行时,才能够确定变量的作用范围和可见性。与词法作用域相对,它是一种动态作用域,函数的作用域会因为调用对象不同而发生变化。

# 全局作用

代码任意地方都可以访问到

# 局部作用域(函数作用域)

只有函数内部才能访问

# 块级作用域

ES6中定义 {var a =10 }会生成块级作用域

# 作用域链

查找变量的过程就叫做作用域链。

一般情况下当前执行上下文的变量取值会到当前上下文的变量对象中取值。 但是如果在当前变量对象中没有查到值,就会向上层环境中变量对象的去查,直到查到全局作用域,这个查找过程形成的链条就叫做作用域链。

作用域链在函数调用的时候创建出来, 它包含了活动对象(变量对象在执行阶段变为活动对象)和该函数的内部**[[Scope]]**属性。函数的Scope 属性是函数定义的时候创建的,这个属性对应的是一个对象列表。该列表中存储着与之相关联作用域的变量对象,该对象仅能js内部访问。因此,如果用一个数组scopeList来模拟作用域链,则scopeList[0]即代表当前上下文的活动对象,其余元素[scope]属性中的所有对象的顺序排列。该数组的最末端是全局变量对象

# 原型

每个对象都有__proto__【原型】属性指向构造它的构造函数的原型对象。通过原型会形成原型链

# 原型对象

每个函数都有一个prototype属性,这个属性是指向一个对象的引用,这个对象称为原型对象。

# 原型链

对象查找原型的过程

当对象查找一个属性的时候,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,此时原型为 null,查找停止。这种通过 通过原型链接的逐级向上的查找链被称为原型链

# 闭包

如果函数引用了外部变量的值,则JavaScript引擎会为该函数创建一个闭包体(closure),闭包体是一个完全封闭和独立的作用域,它不会在函数调用完毕后就被JavaScript引擎当做垃圾进行回收。闭包体可以长期存在,因此开发人员常把闭包体当做内存中的蓄水池,专门用来长期保存变量的值。只有当闭包体的外部引用被全部设置为null值时,该闭包才会被回收。当然,也容易引发垃圾泛滥,甚至出现内存外溢的现象。

  • 访问函数内部变量
  • 突破作用域链
  • 数据缓存
  • 共享私有状态

# redcue

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

reducer 函数接收4个参数:

  • Accumulator (acc) (累计器) - 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。
  • Current Value (cur) (当前值) - 数组中正在处理的元素。
  • Current Index (idx) (当前索引) 数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则从索引1起始。
  • Source Array (src) (源数组) - 调用reduce()的数组

initialValue可选- 作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

回调函数第一次执行时,accumulator 和currentValue的取值有两种情况:如果调用reduce()时提供了initialValue,accumulator取值为initialValue,currentValue取数组中的第一个值;如果没有提供 initialValue,那么accumulator取数组中的第一个值,currentValue取数组中的第二个值。

::: tips 如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。 :::

[0, 1, 2, 3, 4].reduce(function(accumulator, currentValue, currentIndex, array){
  return accumulator + currentValue;
});


1
2
3
4
5

callback 被调用四次,每次调用的参数和返回值如下表:

callback accumulator currentValue currentIndex array return value
first call 0 1 1 [0,1,2,3,4] 1
second call 1 2 2 [0,1,2,3,4] 3
third call 3 3 3 [0,1,2,3,4] 6
fourth call 6 4 4 [0,1,2,3,4] 10

# reduceRight (opens new window)

Array.prototype.reduceRight = function(callback /*, initialValue*/) {
    'use strict';
    if (null === this || 'undefined' === typeof this) {
      throw new TypeError('Array.prototype.reduceRight called on null or undefined');
    }
    if ('function' !== typeof callback) {
      throw new TypeError(callback + ' is not a function');
    }
    var t = Object(this), len = t.length >>> 0, k = len - 1, value;
    if (arguments.length >= 2) {
      value = arguments[1];
    } else {
      while (k >= 0 && !(k in t)) {
        k--;
      }
      if (k < 0) {
        throw new TypeError('reduceRight of empty array with no initial value');
      }
      value = t[k--];
    }
    for (; k >= 0; k--) {
      if (k in t) {
        value = callback(value, t[k], k, t);
      }
    }
    return value;
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# compose

组合函数的概念简单,它只是简单地结合了多个函数。它是一个从右向左流动的函数,用上一个函数的输出调用每个函数。

/**
 * Function Composition is way in which result of one function can
 * be passed to another and so on.
 *
 * h(x) = f(g(x))
 *
 * Function execution happens right to left
 *
 * https://en.wikipedia.org/wiki/Function_composition
 */

const compose = (...args) => (value) => args.reduceRight((acc, fn) => fn(acc), value)

// Increament passed number
const inc = (n) => n + 1

// Doubles the passed value
const double = (n) => n * 2

// using composition function
console.log(compose(double, inc)(2)); // 6

// using composition function
console.log(compose(inc, double)(2)); // 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 高阶函数

操作函数的函数,它接受一个函数或多个函数作为参数,并返回一个新函数

function not(f) {
    return function() {
        var result = f.apply(this,arguments)
        retutn !result
    }
}

not 就是高阶函数,它接受一个函数,并返回一个函数
1
2
3
4
5
6
7
8

# 偏函数

function array(a,n) {
    return Array.prototype.slice.call(a,n ||0)
}

function partialLeft(f) {
    var args = arguments;
    return function() {
        var a = array(args, 1);
        a = a.concat(array(arguments))
        return f.apply(this,a)
    }
}

function partialRight(f) {
    var args = arguments;
    return function() {
        var a = array(arguments)
        a = a.concat(array(args, 1))
        return f.apply(this,a)
    }
}

function partial(f) {
    var args = arguments;
    return function() {
        var a = array(args,1);
        var i=0,j=0;
        for(;i < a.length;i++) {
            if(a[i] === undefined) a[i] = arguments[j++];
        }
        a = a.concat(array(arguments,j))
        return f.apply(this, a)
    }
}


var f = function (x,y,z) { return x * (y - z)}
partialLeft(f,2)(3,4) // 2 * (3 -4 )

partialRight(f,2)(3,4) // 3 * (4-2)

partial(f, undefined, 2) (3,4)  // 3 * (2-4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 缓存函数

function memorize(f) {
    var cache = {}
    return function() {
        var key = arguments.length + Array.prototype.join.call(arguments, ',')
        if(key in cache) return [key];
        else return cache[key] = f.apply(this,arguments)
    }
}

function gcd(a,b) {
    var t;
    if(a < b) {
        t = b;
        b = a;
        a = t;
    }

    while(b != 0) { 
        t = b
        b = a % b
        a = t
    }
    return a
}
var gcdmemo = memorize(gcd)
gcdmemo(85, 187)



var factorial = memorize(function(n) {
    return (n <=1 ? 1 : n * factorial(n-1))
})
factorial(5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 函数柯力化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

function progressCurrying(fn, args) {

    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

练习


// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
let myAdd = (a, b, c, d) => a+b+c;
function curry(fn, args){
    let len = fn.length;
    let _this = this;
    let _args = args || [];
    return function(){
        let args = Array.prototype.slice.apply(arguments);
        args = Array.prototype.concat.call(_args, args);
        // 当接收到的参数小于fn所需参数个数时,继续接收参数
        if(args.length < len){
            return curry.call(_this, fn, args);
        }
        return fn.apply(this, args);
    }
}
let add = curry(myAdd);
console.log(add(2)(3)(4)(5));  // 9
console.loh(add(2,3)(4));   // 9
console.log(add(2,3,4));    // 9



function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 函数式编程

  1. mori (opens new window)
  2. immutable-js (opens new window)
  3. underscore (opens new window)
  4. lodash (opens new window)
  5. Ramda (opens new window)

# MVVM

# 尾调用

尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。

function B() {
    return 1;
}
function A() {
    return B();  
}
1
2
3
4
5
6

# WebWoker

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法.

Web Worker 使用教程 (opens new window) Web Workers API (opens new window)

# 内存泄露

内存被长时间占用不得释放,导致可用内存越来越小,这种现象就是内存泄露。

使用var定义的全局对象

major = 'JS';
var user = 'Jerry';
function getName() {
  return 'jerry';
}

window.major // => 'JS'
window.user // => 'Jerry'
window.getName() // => 'jerry'


未清除的定时器

const object = {};
const intervalId = setInterval(function() {
  doSomething(object);
}, 2000);


// 回调函数

const element = document.getElementById('button');
const onClick = () => alert('hi');

element.addEventListener('click', onClick);

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);


// DOM操作引起的,现代浏览器不需要处理了。
// IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:
var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 资料

  1. 浅谈Javascript执行上下文与作用域,作用域链 (opens new window)
  2. 前端铁蛋儿-深入剖析闭包 (opens new window)
  3. 搞懂变量提升、this、作用域链、闭包以及(GO,VO,AO)原理 (opens new window)
  4. 运算符优先级 (opens new window)