读《JavaScript核心技术开发解密》笔记

【前言】《JavaScript核心技术开发解密》这本书真的写的很是好,特别适合深刻理解JavaScript的运行机制,我的推荐阅读。如下是我在阅读该书时根据书中章节作的笔记,目的是方便本身加深理解。若是有理解错误的地方,还请指出来。前端

【笔记内容】node

1、数据结构

在JS语言核心中,咱们必须了解三种数据结构:栈(stack)、堆(heap)、队列(queue)。jquery

栈是一种先进后出,后进先出(LIFO)的数据结构,相似于只有一个出入口的羽毛球盒,在JS中,栈能够用来规定代码的执行顺序,例如函数调用栈(call stack)webpack

JS的数组是提供了两个栈操做的方法,es6

push向数组的末尾添加元素(进栈)web

pop取出数组最末尾的一个元素(出栈)面试

堆是一种树形的结构,JS中的对象表示能够当作是一种堆的结构,如:ajax

var root = {
    a: 10,
    b: 20,
    c: {
        d: 30,
        e: 40
    }
}
复制代码

能够画出堆的图示如:正则表达式

image-20190520130628236

那么对应的,JS的对象通常是保存在堆内存中。算法

队列

队列是一种**先进先出(FIFO)**的数据结构,相似于排队过安检同样,理解队列对理解JS的事件循环颇有帮助

2、数据类型和内存空间

基础数据类型

最新的ECMAScript标准号定义了7种数据类型,其中包括6种基本数据类型和一种引用类型

其中,基本数据类型是:Number、String、Boolean、Null、Undefined、Symbol (在ES5中没有Symbol类型),

一种引用数据类型是Object

目前浏览器对Symbol类型的兼容不行,所以建议在实际开发中不使用Symbol

咱们在书写函数的时候,声明变量通常是这样的:

function foo(){
    var num1 = 28;
    var num2 = 39;
    ...
}
复制代码

那,在运行函数foo的时候,它的变量保存在哪里?从JS的内存管理上来看,函数运行时,会建立一个执行环境,这个执行环境叫作执行上下文(EC),在执行上下文中,会建立一个变量对象(VO),即函数内声明的基础数据类型保存在该执行上下文的变量对象中

变量对象是保存在堆内存中的,可是因为变量对象具备特殊功能,因此在理解时,咱们将变量对象与堆内存空间区分开来

引用数据类型

引用数据类型除了Object,数组对象、正则表达式、函数等也属于引用数据类型。其中,引用数据类型的值是保存在堆内存空间中的对象。如:

var o = {
    a: 10,
   	b: { m: 20}
}
复制代码

对于如上代码,o属于引用数据类型,等号右边的内容属于其值,那么{a:10,b:{m:20}}存在堆内存空间,o存在对应的执行上下文的变量对象中,这里的执行上下文为全局。

咱们根据一个例子和图示理解下:

function foo(){
    var num = 28;
    var str = 'hello';
    var obj = null;
    var b = { m: 20 };
    var c = [1,2,3];
    ...
}
复制代码

如图,当咱们想要访问对象b的内容时,其实是经过一个引用(地址指针)来访问的:

image-20190520140256862

咱们再来思考两个问题:

var a = 20;
var b = a;
b = 30;
console.log(a);  // 此时输出多少?
var m = {x:10, y:20};
var n = m;
n.y = 30;
console.log(m.y); // 此时输出多少?
复制代码

输出的结果是20 30,若是可以理解这两个输出,那么相信你对于引用和JS的内存管理是理解了。

内存空间管理

JS有自动垃圾回收机制,当一块内存空间的数据可以被访问时,垃圾回收器就认为该数据暂时未使用完不算垃圾,碰到不须要再使用的内存空间数据时,会将其标记为垃圾,并释放该内存空间,也就是标记-清除算法。这个算法会从全局对象开始查找,相似从树的根节点往下找,进行标记清除。

因此,通常当一个函数执行完以后,其内部的变量对象就会被回收。可是若是是全局变量的话,变量何时释放对于回收器来讲是比较难判断的,咱们为了性能,应该尽可能避免过多的使用全局变量或者在不使用该全局变量时手动设置变量值为null这种方式释放。

3、执行上下文

前面说到,JS在执行一个函数时会建立一个执行上下文。其实,在全局环境下也会有执行上下文,能够笼统的将执行上下文分为局部执行上下文和全局执行上下文。

一个JS程序中只能有一个全局环境,可是能够有不少局部环境,因此可见,在一个JS程序中,一定出现多个执行上下文。JS引擎以函数调用栈的方式来处理执行上下文,其中栈底永远都是全局上下文,栈顶则是当前正在执行的上下文,栈顶的执行上下文执行完毕后,会自动出栈

咱们经过一个实例来理解:

function a(){
    var hello = "Hello";
    var world = "world";
    function b(){
        console.log(hello);
    }
    function c(){
        console.log(world);
    }
    b();
    c();
}

a();
复制代码

第一步,全局上下文入栈,并置于栈底:

image-20190520142921427

第二步,全局上下文入栈后,开始执行全局上下文内部代码,直到遇到a(),a()激活了函数a,从而建立了a的执行上下文,因而a的执行上下文入栈,如图:

image-20190520143147280

第三步,a的执行上下文执行内容,碰到了b()激活了b函数,因此b的执行上下文入栈:

image-20190520143403615

第四步,在b的执行上下文里面,没有能够生成其余执行上下文的状况,因此这段代码能够顺利执行完毕,b的执行上下文出栈。

image-20190520143147280

第五步,b的执行上下文出栈以后,急需执行a的后面内容,碰到了c()激活了c函数,因此c的执行上下文入栈,如图所示:

image-20190520143859026

第六步,在c的执行上下文中,没有其余的生成执行上下文内容,因此当c里面的执行代码结束后,c的执行上下文出栈:

image-20190520144111387

第七步,a接下来的代码也执行完毕,因此接着a的执行上下文出栈

image-20190520142921427

最后,全局上下文在浏览器窗口关闭(或Node程序终止)的时候出栈。

注意:函数执行中,若是碰到return会直接终止可执行代码的执行,所以会直接将当前上下文弹出栈。

总的执行顺序如图:

image-20190520145341482

思考下面的程序从执行上下文来看分为几步?

function a(){
    var hello = "hello";
    function b(){
        console.log(b);
    }
    return b;
}
var result = a();
result();
复制代码

图示以下:

image-20190520145859647

4、变量对象

前面咱们提到过变量对象,在JS代码中声明的全部变量都保存在变量对象中,其中变量对象包含以下内容:

  1. 函数的全部参数
  2. 当前上下文的全部函数声明(经过function声明的函数)
  3. 当前上下文的全部变量声明(经过var声明的变量)

建立过程

  1. 在Chrome浏览器(Node)中,变量对象会首先获取函数的参数变量及值;在Firefox浏览器中,直接将参数对象arguments保存到变量对象中;
  2. 先依次获取当前上下文全部的函数声明,也就是function关键字声明的函数。函数名做为变量对象的属性,其属性值为指向该函数所在的内存地址引用。若是函数名已存在,那么属性值会被新的引用覆盖
  3. 依次获取当前上下文全部的变量声明,也就是var关键字声明的变量。每找到一个变量就在变量对象中建议一个属性,属性值为undefined。若是该变量名已存在,为防止同名函数被修改成undefined,则会直接跳过该变量,原属性值不修改

咱们根据上面的过程,思考下面这一句代码执行的过程:

var a = 30;
复制代码

过程以下:

第一步,上下文的建立阶段会先确认变量对象,而变量对象的建立过程对于变量声明来讲是先获取变量名并赋值为undefined,因此第一步拆解为:

var a = undefined;

复制代码

上下文建立阶段结束后,进入执行阶段,在执行阶段完成变量赋值的工做,因此第二步是:

a = 30;

复制代码

须要注意的是,这两步分别是在上下文的建立阶段和执行阶段完成的,所以var a=undefined是提早到比较早的地方去执行了,也便是变量提高(Hoisting)。因此,咱们如今要有意识,就是JS程序的执行是分为上下文建立阶段和执行阶段的

思考以下代码的执行顺序:

console.log(a);  // 输出什么?
var a = 30;

复制代码

在变量对象的建立过程当中,函数声明的优先级高于变量声明,并且同名的函数会覆盖函数与变量,可是同名的变量并不会覆盖函数。不过在上下文的执行阶段,同名的函数会被变量从新赋值。

以下代码中:

var a = 20;
function fn(){ console.log('函数1') };
function fn(){ console.log('函数2') };
function a(){ console.log('函数a') };


fn();
var fn = '我是变量可是我要覆盖函数';
console.log(fn);
console.log(a);

// 输出:
// 函数2
// 我是变量可是我要覆盖函数
// 20

复制代码

上面例子执行的顺序能够当作:

/** 建立阶段 **/
// 函数变量先提高
function fn(){ console.log('函数1') };
function fn(){ console.log('函数2') };
function a(){ console.log('函数a') };
// 普通变量接着提高
var a = undefined; 
var fn = undefined;  // 建立阶段即便同名,可是变量的值不会覆盖函数值

/** 执行阶段 **/
a = 20;
fn();
fn = '我是变量可是我要覆盖函数';
console.log(fn);
console.log(a);

复制代码

实例分析

function foo(){
    console.log(a);
    console.log(fn());
    
    var a = 1;
    function fn(){
        return 2;
    }
}
foo();

复制代码

运行foo函数时,对应的上下文建立,咱们使用以下形式来表达这个过程:

/** 建立过程 **/
fooEC(foo的执行上下文) = {
    VO: {},		// 变量对象
    scopeChain: [],		// 做用域链
    this: {}	
}

// 这里暂时不讨论做用域与this对象

// 其中,VO含以下内容
VO = {
    arguments: {...},
    fn: <fn reference>, a: undefined } 复制代码

建立过程当中会建立变量对象,因此如上形式所示。在函数调用栈中,若是当前上下文在栈顶,那就开始执行,此时变量对象称为活动对象(AO,Activation Object):

/** 执行阶段 **/
VO -> AO
AO = {
    arguments: {},
    fn: <fn reference>, a: 1 } 复制代码

因此,这段代码的执行顺序应该为:

function foo(){
    function fn(){
    	return 2;
    }
    var a = undefined;
    console.log(a);
    console.log(fn());
    a = 1;
}
foo();

复制代码

全局上下文的变量对象

以浏览器为例,全局变量对象为window对象。而在node中,全局变量对象是global。

windowEC = {
    VO: window,
    this: window,
    scopeChain: []
}

复制代码

5、做用域与做用域链

在其余的语言中,咱们确定也据说过做用域这个词,做用域是用来规定变量与函数可访问范围的一套规则

种类

在JS中,做用域分为全局做用域与函数做用域。

全局做用域

全局做用域中声明的变量与函数能够在代码的任何地方被访问。

如何建立全局做用域下的变量:

  1. 全局对象拥有的属性与方法

    window.alert
    window.console
    
    复制代码
  2. 在最外层声明的变量与方法

    var a = 1;
    function foo(){...}
    
    复制代码
  3. 非严格模式下,不使用关键字声明的变量和方法【不要使用!!】

    function foo(){
        a = 1;    // a会成为全局变量
    }
    
    复制代码

函数做用域

函数做用域中声明的变量与方法,只能被下层子做用域访问,而不能被其余不相干的做用域访问。

例如:

function foo(){
    var a = 1;
    var b = 2;
}
foo();
function sum(){
    return a+b;
}
sum(); // 执行报错,由于sum没法访问到foo做用域下的a和b

复制代码

可是像下面这样写是对的:

function foo(){
    var a = 1;
    var b = 2;
    function sum(){
        return a+b;
    }
    sum();	// 能够访问,由于sum的做用域是foo做用域的子做用域
}
foo();

复制代码

在ES6以前,ECMAScript没有块级做用域,所以使用时须要特别注意,必定是在函数环境中才能够生成新的做用域。而ES6以后,咱们能够经过用let来声明变量或方法,这样它们就能在"{"和"}"之间造成块级做用域

模拟块级做用域

咱们能够经过函数来模拟块级做用域,以下:

var arr = [1,2,3,4];

(function(){
    for (var i=0; i< arr.length; i++ ){
        console.log(i);
    }
})();

console.log(i); // 输出undefined,由于i在函数做用域里

复制代码

这种函数叫作当即执行函数

写法大体有以下几种,建议第一种写法:

(function(){
    ...
})();

!function(){
    ...
}();
    
// 把!改为+或-也能够

复制代码

在ECMAScript开发中,咱们可能会常用当即执行函数方式来实现模块化。

做用域链

function a(){
    ...
    function b(){
        ...
    }
}
a();

复制代码

如上伪代码中,前后建立了全局函数a和函数b的执行上下文,假设加上全局上下文,它们的变量对象是VO(global),VO(a)和VO(b),那么b的做用域链则同时包含了这三个变量对象,以下:

bEC = {
    VO: {...},
    scopeChain: [VO(b), VO(a), VO(global)],  //做用域
    this: {}
}

复制代码

做用域链是在代码执行阶段建立的,理解做用域链是学习闭包的前提,闭包里面会有更多的对做用域链的应用

6、闭包

什么是闭包

简单来说的话,闭包就是指有权访问另外一个函数做用域中变量的函数,其本质是在函数外部保持了内部变量的引用,。

建立一个闭包最多见的方式,就是在一个函数内部建立另外一个函数,这个函数就是闭包,以下:

function foo(){
    var a = 1;
    var b = 2;
    
    function closure(){
        return a + b;
    }
    
    return closure;
}

复制代码

上面的代码中,closure函数就是一个闭包(在chrome的调试里面,用闭包的父做用域函数名表示),闭包的做用域为[VO(closure), VO(foo), VO(global)]

根据上面的理解,咱们来看一个例子,里面有闭包吗:

var name = 'window';
var obj = {
    name: 'my object',
    getName: function(){
        return function(){
            return this.name
        }
    }
}
console.log( obj.getName()() )  // 输出: window

复制代码

在这个例子中,虽然在getName函数里面,用了一个内部函数,可是咱们发现最终返回的this.name输出window,能够看出这不是一个闭包。由于其返回的是个匿名函数,而匿名函数的执行上下文是全局上下文,所以其this对象一般指向全局对象,因此this.name输出了window。那咱们怎么修改可让其返回obj的name属性呢?

以下:

var name = 'window';
var obj = {
    name: 'my object',
    getName: function(){
        var _this = this;
        return function(){
            return _this.name
        }
    }
}
console.log( obj.getName()() )  // 输出: my object

复制代码

总结下,就是闭包的做用域链必须是包含了他的父级函数做用域,使用了父级做用域的变量对象下的变量确定就包含了父级做用域

闭包和垃圾回收机制

咱们来回顾下垃圾回收机制:当一个值再也不被引用时会被标记而后清除,当一个函数的执行上下文运行完毕后,内部全部的内容都会失去引用而被清除。

闭包的本质是保持在外部对函数的引用,因此闭包会阻止垃圾回收机制进行回收。

例如一下代码:

function foo(){
    var n = 1;
    nAdd = function(){
        n += 1;
    }
    return function fn(){
        console.log(n);
    }
}
var result = foo();
result();      // 1
nAdd();
result();      // 2

复制代码

由于nAdd和fn函数都访问了foo的n变量,因此它们都与foo造成了闭包。这个时候变量n的引用被保存了下来。

因此,在使用闭包时应该警戒,滥用闭包,极可能会由于内存缘由致使程序性能过差

闭包的应用场景

回顾下,使用闭包后,任何在函数中定义的变量,均可以认为是私有变量,由于不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和函数定义的其余函数。

循环、setTimeout与闭包

咱们先来看一个面试常见的例子:

for( var i=0; i<5; i++ ){
    setTimeout(function timer(){
        console.log(i);
    }, i*1000);
}

复制代码

可能乍一看会以为每隔1秒从0输出到4,可是实际的运行是每隔1秒输出一个5。

咱们来分析一下:

  1. for循环不能造成本身的做用域,因此i是全局变量,会随着循环递增,循环结束后为5
  2. 在每一次循环中,setTimeout的第二个参数访问的都是当前的i,所以第二个参数中i分别为0,1,2,3,4
  3. 第一个参数timer访问的是timer函数执行时的i,因为延迟缘由,当timer开始执行时,此时i已经为5了

若是咱们要隔秒输出0,1,2,3,4,那就须要让for循环造成本身的做用域,因此须要借助闭包的特性,将每个i值用一个闭包保存起来。以下代码:

for( var i=0; i<5; i++ ){
    (function(i){
        setTimeout(function timer(){
            console.log(i);
        }, i*1000);
    })(i);
}

复制代码

固然,在ES6或更高版本中,能够直接使用let关键字造成for的块级做用域,这样也是OK的:

for( let i=0; i<5; i++ ){
    setTimeout(function timer(){
        console.log(i);
    }, i*1000);
}

复制代码

单例模式与闭包

JavaScript也有许多解决特定问题的编码思惟(设计模式),例如咱们常听到过的工厂模式、订阅通知模式、装饰模式、单例模式等。其中,单例模式是最经常使用也是最简单的一种,咱们尝试用闭包来实现它。

其实在JS中,对象字面量就是一个单例对象。以下:

var student = {
    name: 'zeus',
    age: 18,
    getName: function(){
        return this.name;
    },
    getAge: function(){
        return this.age;
    }
}
student.getName();
student.name;

复制代码

可是,这种对象的变量很容易被外部修改,不符合咱们的需求,咱们指望创建本身的私有属性和方法。以下:

var student = (function(){
    var name = 'zeus';
    var age = 18;
    
    return {  // 外部可访问内容
        getName: function(){
            return name;
        },
        getAge: function(){
            return age;
        }
    }
})();
student.getName();
student.name;  // undefined

复制代码

如上,第二个例子中,在当即函数执行的时候就返回student对象了,下面咱们写一个例子,在调用时才初始化:

var student = (function(){
    var name = 'zeus';
    var age = 18;

    var instance = null; // 定义一个变量,用来保存实例
    
    function init(){
        return {
            getName: function(){
                return name;
            },
            getAge: function(){
                return age;
            }
        }
    }

    return {
        getInstance: function(){
            if ( !instance ){
                instance = init();
            }
            return instance;
        }
    }

    
})();
var student1 = student.getInstance();
var student2 = student.getInstance();
console.log( student1 === student2 );  // true

复制代码

模块化与闭包

提出一个问题:若是想在全部的地方都能访问同一个变量,应该怎么作?例如全局的动态管理。

解决方案:使用全局变量(可是时间开发中,不建议轻易使用全局变量)。

其实,模块化的思想能够帮助咱们解决这个问题。

模块化开发是目前最流行,也是必需要掌握的一种开发思路。而模块化实际上是创建在单例模式上的,所以模块化开发和闭包息息相关。目前好比Node里的require,ES6的import和modules等,实现方式不一样可是核心思路是同样的

模块化架构通常须要实现下面三个内容:

1.每个单例就是一个模块,在当前的一些模块化开发中,每个文件是一个模块

2.每一个模块必须有获取其余模块的能力

如在一些模块化开发中,使用require或者import来获取其余模块内容

3.每个模块都应该有对外的接口,以保证与其余模块交互的能力

在一些模块化开发中使用module.exports或者export default {}等将容许其余模块使用的接口暴露出来

咱们今天使用单例模式,来实现简单的模块化思想,案例实现的是每隔一秒,body的背景色就随着一个数字的递增在固定的三个颜色之间切换:

/** * 管理全局状态模块,含有私有变量并暴露两个方法来获取和设置其内部私有变量 */
var module_status = (function(){
    var status = {
        number: 0,
        color: null
    }

    var get = function(prop){
        return status[prop];
    }

    var set = function(prop, value){
        status[prop] = value;
    }

    return {
        get: get,
        set: set
    }
})();
/** * 负责body背景颜色改变的模块 */
var module_color = (function(){
    // 伪装用这种方式执行第二步引用模块
    var state = module_status;

    var colors = ['#c31a86', 'orange', '#ccc'];

    function render(){
        var color = colors[ state.get('number') % 3];
        document.body.style.backgroundColor = color;
    }

    return {
        render: render
    }

})();
/** * 负责显示当前number值模块,用于参考对比 */
var module_context = (function(){
    var state = module_status;

    function render(){
        document.body.innerHTML = 'this Number is '+state.get('number');
    }

    return {
        render: render
    }

})();
/** * 主模块,借助上面的功能模块实现咱们须要的功能 */
var module_main = (function(){
    var state = module_status;
    var color = module_color;
    var context = module_context;

    setInterval(function(){
        var newNumber = state.get('number') + 1;
        state.set('number', newNumber);

        color.render();
        context.render();
    }, 1000);
})();

复制代码

本身分析整个完整的代码以后,真的颇有帮助

7、this对象

上面六大节的内容,能够算是JavaScript的进阶,但其实应该算是JavaScript的基础,具有这些知识的时候再来看this对象这一节,收获会很大。在JS中,最重要的部分就是理解闭包和this!

咱们来回顾下执行上下文和变量对象那一节,咱们知道在函数被调用执行时,变量对象VO会生成,这个时候,this的指向会肯定。所以,必须牢记当前函数的this是在函数被调用执行的时候才肯定的,也就是说this对象须要当前执行上下文在函数调用栈的栈顶时,VO变成AO,同时this的指向肯定。

以下代码:

var a = 10;
var obj = {
    a: 20
}
function fn(){
    console.log(this.a);
}
fn();  // 10
fn.call(obj); // 20

复制代码

代码里面,fn函数里的this分别指向了window(全局对象变量)与obj

全局对象的this

全局对象的变量对象是一个比较特殊的存在,在全局对象中,this指向它自己,因此比较简单

this.a1 = 10;
var a2 = 20;
a3 = 30;

console.log(a1); //10
console.log(a2); //20
console.log(a3); //30

复制代码

以上的用法都会构建全局变量,且在非严格模式语法上没有错误。

函数中的this

在本节第一个例子中,咱们看到,同一个函数的this因为调用方式不一样致使this的指向不一样,所以,在函数中,this最终指向谁,与调用该函数的方式有关。

在一个函数的执行上下文中,this由该函数的调用者提供,由调用函数的方式来决定其指向

以下例子:

function fn(){
    console.log(this);
}
fn();	// fn为调用者,独立调用,非函数的严格模式下指向全局对象window

复制代码

若是调用者被某个对象拥有,那么在调用该函数时,函数内部的this指向该对象。若是调用者函数独立调用,那么该函数内部this指向undefined,可是在非严格模式下,当this指向undefined时,它会指向全局对象。

function fn(){
 'use strict';
    console.log(this);
}
fn();   // undefined
window.fn();  // window

复制代码

思考一下,以下这个例子返回什么:

var a = 20;
var obj = {
    a: 30
}
function fn(){
    console.log('fn this:', this);
    function foo(){
        console.log(this.a);
    }
    foo();
}
fn.call(obj);
fn();

复制代码

另外,对象字面量不会产生做用域,因此以下

'use strict';
var obj = {
 a: 1,
 b: this.a+1
}

复制代码

严格模式下会报语法错误,非严格模式下this指向全局对象

思考下面的例子:

var a = 10;
var foo = {
 a: 20,
 getA: function(){
     return this.a;
 }
}

console.log( foo.getA() );  // 20

var test = foo.getA();
console.log( test() );	// 10,这里为何是10?

复制代码

由于test在执行时,test是调用者,它是独立调用,在非严格模式下,其this指向全局对象

思考以下代码输出什么:

function foo(){
    console.log(this.a);
}

function active(fn){
    fn();
}

var a = 20;
var obj = {
    a: 10,
    getA: foo,
    active: active
}

active(obj.getA);
obj.active(obj.getA);

复制代码

call/apply/bind显式的指定this

JS内部提供了一种能够手动设置函数内部this指向的方式,就是call/apply/bind。全部的函数均可以调用这三个方法。

看以下例子:

var a = 20;
var obj = {
    a: 30
}
function foo(num1,num2){
    console.log(this.a+num1+num2);
}

复制代码

咱们知道,直接调用foo(10,10)的话会打印40,若是咱们想把obj里的a打印出来,咱们像下面这样写:

foo.call(obj,10,10);	// 50
// 或
foo.apply(obj, [10,10]);  // 50

复制代码

那其实call/apply表示将第一个参数做为该函数执行的this对象指向,而后当即执行函数。

call和apply有一点区别,就是传参的区别:

在call中,第一个参数是函数内部this的指向,后续参数则是函数执行时所需参数;

在apply中,只有两个参数,第一个参数是函数内部this的指向,第二个参数是一个数组,数组里面是函数执行所需参数。

bind方法用法与call方法同样,与call惟一不一样的是,bind不会当即执行函数,而是直接返回一个新的函数,而且新的函数内部this指向bind方法的第一个参数

8、函数与函数式编程

其实,咱们仔细回顾下会发现,前面的一到七节的内容基本上都是在围绕函数展开的,让咱们更加清晰的认识函数,这一节主要了解如何运用函数

函数

函数的形式有四种:函数声明、函数表达式、匿名函数与当即执行函数。

1.函数声明

关键字function,从前面的执行上下文建立过程咱们知道function关键字声明的变量比var关键字声明的变量有更高的优先执行顺序,因此变量提高中,先提高函数变量。

function fn(){ ... }

复制代码

2.函数表达式

指将一个函数体赋值给一个变量的过程

var fn = function(){ ... }

复制代码

能够理解为:

// 建立阶段
var fn = undefined;
// 执行阶段
fn = function(){ ... }

复制代码

因此使用函数表达式时,必需要考虑函数使用的前后顺序:

fn();  // TypeError: fn is not a function

var fn = function(){ console.log('hello') }

复制代码

请问,若是在函数表达式里面有this,那这个this指向什么?

3.匿名函数

就是指没有名字的函数,通常会做为参数或返回值来使用,一般不使用变量来保存它的引用。

匿名函数不必定就是闭包,匿名函数能够做为普通函数来理解,而闭包的造成条件,仅仅是有的时候或者匿名函数有关而已

4.当即执行函数

当即执行函数是匿名函数一个很是重要的应用场景,由于函数能够产生做用域,因此咱们常用当即执行函数来模拟块级做用域,并进一步在此基础上实现模块化的运用。

函数式编程

函数式编程其实就是将一些功能、逻辑等封装起来以便使用,减小重复编码量。函数式编程的内涵就是函数封装思想。怎么去封装,学习前辈优秀的封装习惯。让本身的代码看上去更加专业可靠是咱们学习的目的。

1.函数是一等公民

一等公民也就是说函数跟其余的变量同样,没有什么特殊的,咱们能够像对待任何数据类型同样对待函数。

  • 把函数赋值给一个变量

    var fn = function(){}
    
    复制代码
  • 把函数做为形参

    function foo(a, b, callback){
        callback(a+b);
    }
    function fn(res){
        console.log(res);
    }
    foo(2,3,fn);  // 5
    
    复制代码
  • 函数做为返回值

    function foo(x){
        return function(y){
            console.log(x+y);
        }
    }
    foo(2)(3);	// 5
    
    复制代码

2.纯函数

相同的输入总会获得相同的值,而且不会产生反作用的函数,叫作纯函数。

例如咱们想封装一个获取数组最后一项的方法,有两种选择:

// 第一种
function getLast1(arr){
    return arr[arr.length];
}

// 第二种
function getLast2(arr){
    return arr.pop();
}

复制代码

getLast1和getLast2虽然均可以知足需求,可是getLast2在使用以后会改变arr数组内容,下一次再使用的话,因为arr最后一个值已经被取出,致使第二次使用取到的值是原来值的倒数第二个值。因此,像第二种这样的封装是很是糟糕的,会将原数据弄得特别混乱。在JavaScript的标准函数里,也有许多不纯的方法,咱们在使用时要多注意。

3.高阶函数

能够粗暴的理解,凡是接收一个函数做为参数的函数,就是高阶函数。可是这样就太广义了,高阶函数实际上是一个高度封装的过程,

咱们来尝试封装一个方法mapArray(array, fn),其有两个参数,第一个参数是一个数组,第二个参数是一个函数,其中第二个参数参数有两个参数fn(item, index)第一个item表示是数组每次遍历的值,第二个是每次遍历的序号。

var a = [1,2,3,4,5];

function mapArray(array, fn){
    var temp = [];
    if ( typeof fn === "function" ){
        for ( var k=0; k<array.length; k++ ){
            temp.push( fn.call(array, array[k], k) );
        }
    } else {
        console.error('TypeError' + fn + ' is not a function.');
    }
    return temp;
}

var b = mapArray(a, function(item, index){
    console.log(this.length);  // 5
    return item + 3;
});

console.log(b);  // [4,5,6,7,8]
复制代码

mapArray函数实现了将数组里的每一项都进行了相同的操做,而且在第二个函数里的this指向的是第一个数组参数对象。

从这个封装函数来看,实际上是把数组的循环给封装了,那就是说,咱们要封装的就是程序公用的那一部分,而具体要作什么事情,则以一个参数的形式,来让使用者自定义。这个被当作参数传入的函数就是基础函数,而咱们封装的mapArray方法,就能够称之为高阶函数

高阶函数实际上是一个封装公共逻辑的过程

4.柯里化函数

暂时不说,由于比较难,我须要仔细理清以后再写

9、面向对象

虽然JS是面向对象的高级语言,可是它与Java等一类语言不一样,在ES6以前是没有class的概念的,基于原型的构建让你们深刻理解JavaScript的面向对象有点困难。难点就是重点,因此JS的面向对象确定是须要咱们去了解的

在EcmaScript-262中,JS对象被定义为**"无序属性的集合,其属性能够包含基本值、对象或者函数"**。

对象字面量

从上面的定义中,对象是由一系列的key-value对组成,其中value为基本数据类型或对象,数组,函数等。像这种形式的对象定义格式,叫作对象字面量,如:

var Student = {
    name: 'ZEUS',
    age: 18,
    getName: function(){
        return this.name;
    }
    parent: {}
}
复制代码

建立对象

第一种,经过关键字new来建立一个对象:

var obj = new Object();		// new 后面接的是构造函数
复制代码

第二种,使用对象字面量:

var obj = {};
复制代码

咱们要给对象建立属性或方法时,能够像这样:

// 第一种方式
var person = {};
person.name = 'zeus';
person.getName = function(){
    return this.name;
}

// 第二种方式
var person = {
    name: 'zeus',
    getName: function(){
        return this.name;
    }
}
复制代码

访问对象的方法或属性,可使用.或者 ['']

构造函数与原型

在函数式编程那一节,咱们讲到封装函数就是封装一些公共逻辑与功能。当面对具备同一类事物时,咱们也能够借助构造函数与原型的方式,将这类事物封装成对象

例如:

var Student = function(name, age){
    this.name = name;
    this.age = age;
    console.log(this);
}
Student.prototype.getName = function(){
    return this.name;
}

// 实例化对象时
var zeus = new Student('zeus', 18);  // zeus实例
zeus.getName();
Student('zeus', 18);   // window
复制代码

能够看到,具体的某个学生的特定属性,一般放在构造函数中;全部学生的方法和属性,一般放在原型对象中。

上述代码输出内容以下图:

image-20190531113004351

这里提个问,构造函数是高阶函数吗?在这里,new Student()内部的this为何会指向实例对象呢,而Student()内部this指向window?

构造函数名约定首字母大写,这里必需要注意。构造函数的this与原型方法中的this指向的都是当前实例。像上面,使用了new关键字以后,Student()函数才是构造函数。那new关键字具体作了什么呢?咱们能够来用一个函数模拟new关键字的能力:

function New(func){
    var res = {};
    if ( func.prototype !== null ){
        res.__proto__ = func.prototype; 
    }
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1) );
    // 当咱们在构造函数中明确指定了返回对象时,进行这一步
    if ( (typeof ret ==="object" || typeof ret==="function" ) && ret !== null ){
        return ret;
    }
    // 若是没有明确指定返回对象,则默认返回res,这个res就是实例对象
    return res;
}
复制代码

经过对New方法的封装,能够知道new关键字在建立实例时经历了以下过程:

  1. 先建立一个新的、空的实例对象;
  2. 将实例对象的原型(__proto__),指向构造函数的原型(prototype);
  3. 将构造函数内部的this,修改成指向实例;
  4. 最后返回该实例对象

构造函数、原型、实例之间的关系

咱们可不能够在构造函数里面建立方法?固然是能够的,可是这样比较消耗更多的内存空间,由于每一次建立实例对象,都会建立一次该方法。

因此能够看出,在构造函数里声明的变量与方法只属于当前实例,所以咱们能够将构造函数中声明的属性与方法看作该实例的私有属性和方法,它们只能被当前实例访问。而原型中的属性与方法可以被全部的实例访问,所以能够将原型中声明的属性和方法称为公有属性与方法。若是构造函数里的私有属性/方法与原型里的公有属性/方法重名,那么会优先访问私有属性/方法

怎么判断一个对象是否拥有某一个方法/属性

  1. 经过in运算符来判断,不管该方法/属性是否公有,只要存在就返回true,不然返回false
  2. 经过hasOwnProperty方法来判断,只有该方法/属性存在且为私有时,才返回true,不然返回false
var Student = function(name, age){
this.name = name;
this.age = age;
this.speak = function(){
   console.log('我是'+this.name+'的私有方法')
}
}

Student.prototype.getName = function(){
console.log(this.name);
}

var Bob = new Student('Bob', 18);
Bob.speak();
Bob.getName();

console.log( 'speak' in Bob);  // true
console.log( 'getName' in Bob);  // false
console.log( Bob.hasOwnProperty('speak') );  // true
console.log( Bob.hasOwnProperty('getName') );  // false
复制代码

若是要在原型上添加多个方法,还能够这样写:

function Student(){};
Student.prototype = {
    constructor: Student,    // 必须声明
    getName: function(){},
    getAge: function(){}
}
复制代码

原型对象

原型对象其实也是普通对象。在JS中,几乎全部的对象均可以是原型对象,也能够是实例对象,还能够是构造函数,甚至身兼数职。当一个对象身兼多职时,它就能够被看做原型链中的一个节点。

当要判断一个对象student是不是构造函数Student的实例时,可使用instanceof关键字,其返回一个boolean值:

student instanceof Student;    // true or false
复制代码

咱们回到最开始的时候,当建立一个对象时,除了使用对象字面量也可使用new Object()来建立,所以Object实际上是一个构造函数,而其对应的原型Object.prototype则是原型链的终点。

当建立函数时,除了使用function关键字外,还可使用Function对象:

var add = new Function("a", "b", "return a+b");
// 等价于
var add = function(a, b){
    return a+b;
}
复制代码

在这里,add方法是一个实例对象,它对应的构造函数是Function,它的原型是Function.prototype,也就是add.__proto__ === Function.prototype。这里比较特殊的是,Function同时是Function.prototype的构造函数与实例(由于Function也是一个函数啦!);而与此同时,由于Function是继承自Object的,因此Function.prototype仍是Object.prototype的实例,它们的原型链能够用下图表示:

add函数相关的原型链

对原型链上的方法与属性的访问,与做用域链类似,也是一个单向的查找过程,虽然add与Object原型没有直接关系,可是它们在同一条原型链上,所以add也可使用Object的toString方法等(好比hasOwnProperty方法)。

实例方法,原型方法,静态方法

看以下代码便可了解:

function Foo(){
    this.bar = function(){     // 实例(私有)方法
        return 'bar in Foo';    
    }
}

Foo.bar = function(){		// 静态方法,不须要实例化,直接能够用函数名调用
    return 'bar in static';	
}

Foo.prototype.bar = function(){		// 原型方法
    return 'bar in prototype';
}
复制代码

继承

由于封装一个对象是由构造函数与原型共同组成的,因此继承也被分为两部分,一部分是构造函数继承另外一部分是原型继承。

以下代码:

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

Person.prototype.say = function(){
    console.log('您好');
}

var Student = function(name, age, grade){
    Person.call(this, name, age);  // 在这里是构造继承
    this.grade = grade;
}
// 下面这两句是原型继承
Student.prototype = new Person();
Student.prototype.constructor = Student;  // 这句必定不能少
Student.prototype.speak = function(){
    console.log(`我叫${this.name},我今年${this.age}岁了,我语文考了${this.grade}分`);
}

var kevin = new Student('kevin', 18, 90);
kevin.speak();
kevin.say();
复制代码

这段代码属于组合继承,是比较经常使用的一种继承方式,不过他有个不足就是,不管什么状况下都会调用两次父级构造函数。

以下是优化以后的代码:

function inheritPrototype(child, parent){
    var obj = Object(parent.prototype);
    obj.prototype = child;
    child.prototype = obj;
}

var Person = function(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log('您好');
}

var Student = function(name, age, grade){
    Person.call(this, name, age);
    this.grade = grade;
}
inheritPrototype(Student, Person);
Student.prototype.speak = function(){
    console.log(`我叫${this.name},我今年${this.age}岁了,我语文考了${this.grade}分`);
}

var kevin = new Student('kevin', 18, 90);
kevin.speak();
kevin.say();
复制代码

这一段是寄生组合式继承,是开发人员认为的引用类型最理想的继承方式。

10、ES6基础

ES6是ECMAScript6的简称,也被称为ECMAScript2015。是目前兼容性比较乐观且比较新的ECMAScript标准,虽然增长了前端的学习成本,可是与ES5相比,它提供了不少新的特性,并且如今前端基本上都在转ES6了,因此ES6也是学习前端的必备基础。不过目前,并非全部的浏览器都支持ES6新特性,可是在开发中,咱们能够借助babel提供的编译工具,将ES6转化为ES5,这也极大的推进了前端团队对ES6的接受。对于大多数经常使用的ES6新特性,目前最新版的Chrome都已所有支持。不过对于部分知识,例如模块化modules,则须要经过构建工具才可以使用,例如使用webpack和babel的VueJS。

新的变量声明方式let/const

在ES6中,咱们可使用let来声明变量,其中,let会产生变量的块级做用域,而且let在变量提高的时候不会给变量赋值undefined,因此这样使用会直接报错:

console.log(a);  // 不会输出undefined,会直接报ReferenceError
let a = 10;
复制代码

因此,若是你决定用ES6的变量声明来写了,就所有用let吧,不要let和var混用。

const是用来声明一个常量的,该常量的引用地址不可改变。

这里须要注意的是let和const变量的值,都是一个引用,若是对let的变量进行赋值操做,是新建了该值以后将其引用从新赋给变量。

例如:

const a = [];
a.push(1);    // 不会报错
const b = 1;
b = 2;   // 报错Uncaught TypeError: Assignment to constant variable.
复制代码

箭头函数

ES6的箭头函数是一个用起来比较温馨的方式,咱们用例子来看一下:

// ES5中声明函数
var fn = function(a, b){
    return a+b;
}
// ES6箭头函数
var fn = (a, b) => a+b;  // 当函数直接return时,能够省略{}
复制代码

须要注意的是,箭头函数只能替换函数表达式,使用function关键字声明的函数不能使用箭头函数替换,以下形式不能用箭头函数替换:

function fn(a,b){
    return a+b;
}
// 不能够替换成下面形式
fn(a, b)=> a+b;
复制代码

咱们一看到函数就应该去想如下它内部的this在调用时指向谁,从前面知识咱们知道,函数内部的this指向,与它的调用者有关,或者使用call/apply/bind也能够修改函数内部的this指向。

咱们来回顾一下,请思考下面的输出内容:

var name = 'Tom';
var getName = function(){
    console.log(this.name);   
}
var person = {
    name: 'Alex',
    getName: function(){
        console.log(this.name);
    }
}
var other = {
    name: 'Jone'
}
getName();    // ?
person.getName();   // ?
getName.call(other);    // ?
复制代码

上面分别输出了Tom,Alex,Jone,第一个getName()独立调用,其this指向undefined并自动转向window。那假如所有换成箭头函数呢?咱们看一下输出结果:

var name = 'Tom';
var getName = () => {
    console.log(this.name);   
}
var person = {
    name: 'Alex',
    getName: () => {
        console.log(this.name);
    }
}
var other = {
    name: 'Jone'
}
getName();          //Tom
person.getName();   //Tom
getName.call(other);//Tom
复制代码

运行发现,三次都输出了Tom,这也是须要你们注意的地方。箭头函数中的this,就是声明函数时所处的上下文中的this,他不会被其余方式所改变

因此有些场景能够用箭头函数来解决:

document.name = 'doc';
var obj = {
    name: 'zeus',
    do: function(){
        document.onclick = function(){
            console.log(this.name);   // 由于是document调用了该函数,因此点击页面输出doc
        }
    }
}
obj.do();
复制代码

若是咱们要在页面被点击后输出zeus,可能最经常使用的就是在document.onclick外面使用_this/that暂存this的值,以下:

document.name = 'doc';
var obj = {
    name: 'zeus',
    do: function(){
        var _this = this;
        document.onclick = function(){
            console.log(_this.name);   // 使用了_this中间变量,输出zeus
        }
    }
}
obj.do();
复制代码

其实,能够用箭头函数的特性来作:

document.name = 'doc';
var obj = {
    name: 'zeus',
    do: function(){
        document.onclick = () => {
            console.log(this.name);   // 箭头函数的this指向当前上下文
        }
    }
}
obj.do();
复制代码

模板字符串

模板字符串是解决通常的字符串拼接麻烦的问题产生的,它使用反引号`包裹字符串,使用${}包裹变量名,以下代码:

// ES5
var a = 'hello';
var b = 'zeus';
var c = 10;
var s = a + ' '+ b + ' ' + (c+10);  // hello zeus 20
// ES6
var str = `${a} ${b} ${c+10}`;   // hello zeus 20
复制代码

解析结构

解析结构能够很方便的从数组或对象获取值,例如对于以下的对象字面量:

let zeus = {
    name: 'zeus',
    age: 20,
    job: 'Front-end Engineer'
}
复制代码

若是要取值,咱们常常会使用点运算符进行取值,例如zeus.namezeus['age'],当使用解析结构时,能够这样作:

const {name, age, job} = zeus;
console.log(name);
复制代码

固然const表示得到到的值声明为常量,也可使用let或var。咱们还能够给属性变量指定默认值:

const {name = 'kevin', age = 20, job = 'student'} = zeus;
// 若是zeus对象对应属性没有值,则使用前面指定的默认值
复制代码

或者给属性变量从新命名:

const {name: username, age, job} = zeus;
// 后面使用的话就必须使用username
复制代码

数组也可使用解析结构,以下:

let arr = [1,2,3,4];
const [a,b,c,d] = arr;
console.log(a);  // 1
console.log(c);  // 3
复制代码

数组的解析结构的属性变量名能够随意命名,可是是按顺序来一一对应的,而对象解析结构中的属性变量必须跟变量属性命名一致。对象属性的解析结构也能够进行嵌套,例如:

let kevin = {
    name: 'kevin',
    age: 20,
    job: 'Student',
    school: {
    	name: 'smu',
    	addr: '成都'
	}
};
const {school: {name}} = kevin;
console.log(name);  // smu
复制代码

展开运算符

在ES6中,使用...做为展开运算符,它能够展开数组/对象。例如:

const arr1 = [1,2,3];
const arr2 = [...arr1, 4,5,6];   // [1,2,3,4,5,6]
let person_kevin = {
    name: 'kevin',
    age: 20,
    job: 'Student'
};
let student_kevin = {
    ...person_kevin,
    school: {
        name: 'smu',
        addr: '成都'
    }
};
复制代码

展开运算符能够用在函数形参里面,可是只能做为函数的最后一个参数

Promise

异步与同步

同步是指发送一个请求,须要等待直到请求结果返回以后,再继续下一步操做。异步在发送请求后,不会等待而是直接继续下一步操做。

咱们来实现一个异步方法:

function fn(){
    return new Promise((resolve, rejsct) =>{
        setTimeout(function(){
            resolve('执行fn内容');
        },1000);
    });
}
复制代码

可使用async/await来模拟同步效果:

var foo1 = async function(){
    let t = await fn();
    console.log(t);
    console.log('接着执行的内容');
}
foo1();
// 等待1秒后,输出:
// 执行fn内容
// 接着执行的内容
复制代码

若是采用异步操做的话,以下:

var foo2 = function(){
    fn().then(res=>{
        console.log(res);
    });
    console.log('接着执行的内容');
}
foo2();
// 先输出 接着执行的内容
// 等待1秒后
// 输出 执行fn的内容
复制代码

简单用法

咱们应该有使用过jquery的$.ajax()方法,该方法获取后端的值是在参数属性success函数的参数中获取的,假如咱们在第一次ajax请求后,要进行第二次ajax请求而且这一次请求的参数是第一次success获的值,若是还有第三次呢,那就必须这样写:

$.ajax({
    url: '',
    data: {...},
    success: function(res){
        $.ajax({
            data: {res.data},
            success: function(res){
                $.ajax(...)
            }
        })
    }
})
复制代码

这样就造成了常说的“回调地狱”,不过在ES6中,Promise语法能够解决这样的问题。·Promise能够认为是一种任务分发器,将任务分配到Promise队列,执行代码,而后等待代码执行完毕并处理执行结果。简单的用法以下:

var post = function(url, data) {
    return new Promise(function(resolve, reject) {
        $.ajax({
            url: url,
            data: data,
            type: 'POST',
            success: function(res){
                resolve(res);
            },
            error: function(err) {
                reject(err);
            }
        });
    });
}
post('http://127.0.0.1:8080/order', {id:1}).then(function(res){
    // 这里返回成功的内容
}, function(err){
    // 这里是报错信息
});
复制代码

上面的代码封装了jquery的ajax请求,将POST的请求进行了封装,post(..)函数内部返回了一个Promise对象,Promise对象有一个then方法,then方法的第一个参数是resolve回调函数表示成功的操做,第二个参数是reject回调函数表示失败或异常的操做。其实,Promise还有一个catch方法也能够获取reject回调函数,如post也能够这样使用:

post('http://127.0.0.1:8080/order', {id:1}).then(function(res){
    // 这里返回成功的内容
}).catch(function(err){
    // 这里是报错信息
});
复制代码

事件循环机制

后面再单独分享

对象与class

参考阮一峰class介绍

模块化

后面再单独分享

相关文章
相关标签/搜索