1 - 作用域

1.1 作用域概述

通常来说,一段程序代码中所用到的名字并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。

JavaScript(es6前)中的作用域有两种:

  • 全局作用域
  • 局部作用域(函数作用域)

    1.2 全局作用域

    作用于所有代码执行的环境(整个 script 标签内部)或者一个独立的 js 文件。

    1.3 局部作用域

    作用于函数内的代码环境,就是局部作用域。 因为跟函数有关系,所以也称为函数作用域。

    1.4 块级作用域

  • 块作用域由 { } 包括。

  • 在其他编程语言中(如 java、c#等),在 if 语句、循环语句中创建的变量,仅仅只能在本 if 语句、本循环语句中使用,如下面的Java代码: java有块级作用域:if(true){
    int num = 123;
    system.out.print(num); // 123
    }
    system.out.print(num); // 报错以上java代码会报错,是因为代码中 { } 即一块作用域,其中声明的变量 num,在 “{ }” 之外不能使用;而与之类似的JavaScript代码,则不会报错:

Js中没有块级作用域(在ES6之前)
if(true){
var num = 123;
console.log(123); //123
}
console.log(123); //123

2 - 变量的作用域

在JavaScript中,根据作用域的不同,变量可以分为两种:

  • 全局变量
  • 局部变量

    2.1 全局变量

    在全局作用域下声明的变量叫做全局变量(在函数外部定义的变量)。

  • 全局变量在代码的任何位置都可以使用

  • 在全局作用域下 var 声明的变量 是全局变量
  • 特殊情况下,在函数内不使用 var 声明的变量也是全局变量(不建议使用)

    2.2 局部变量

    在局部作用域下声明的变量叫做局部变量(在函数内部定义的变量)

  • 局部变量只能在该函数内部使用

  • 在函数内部 var 声明的变量是局部变量
  • 函数的形参实际上就是局部变量

    2.3 二者区别

  • 全局变量:在任何一个地方都可以使用,只有在浏览器关闭时才会被销毁,因此比较占内存

  • 局部变量:只在函数内部使用,当其所在的代码块被执行时,会被初始化;当代码块运行结束后,就会被销毁,因此更节省内存空间

    3 - 作用域链

    3.1 概念

    只要是代码都一个作用域中,写在函数内部的局部作用域,未写在任何函数内部即在全局作用域中;如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域;根据在[内部函数可以访问外部函数变量]的这种机制,用链式查找决定哪些数据能被内部函数访问,就称作作用域链

    3.2 案例分析1

    function f1() {
    var num = 123;
    function f2() {
    console.log( num );
    }
    f2();
    }
    var num = 456;
    f1();

    3.3 案例分析2

    作用域链:采取就近原则的方式来查找变量最终的值。
    var a = 1;
    function fn1() {
    var a = 2;
    var b = ‘22’;
    fn2();
    function fn2() {
    var a = 3;
    fn3();
    function fn3() {
    var a = 4;
    console.log(a); //a的值 ?
    console.log(b); //b的值 ?
    }
    }
    }
    fn1();

    4 - 预解析(预编译)

    4.1 预解析的相关概念

    JavaScript 代码是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器在运行 JavaScript 代码的时候分为两步:预解析和代码执行。

  • 预解析:在当前作用域下, JS 代码执行之前,浏览器会默认把带有 var 和 function 声明的变量在内存中进行提前声明或者定义。

  • 代码执行: 从上到下执行JS语句。预解析会把变量和函数的声明在代码执行之前执行完成。

    4.2 变量预解析

    预解析也叫做变量、函数提升。
    变量提升(变量预解析): 变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升。
    console.log(num); // 结果是多少?
    var num = 10; // ?
    结果:undefined

注意:变量提升只提升声明,不提升赋值

4.3 函数预解析

函数提升: 函数的声明会被提升到当前作用域的最上面,但是不会调用函数。
fn();
function fn() {
console.log(‘打印’);
}
结果:控制台打印字符串 —- ”打印“

注意:函数声明代表函数整体,所以函数提升后,函数名代表整个函数,但是函数并没有被调用!

4.4 函数表达式声明函数问题

函数表达式创建函数,会执行变量提升,此时接收函数的变量名无法正确的调用:
fn();
var fn = function() {
console.log(‘想不到吧’);
}
结果:报错提示 ”fn is not a function”

解释:该段代码执行之前,会做变量声明提升,fn在提升之后的值是undefined;而fn调用是在fn被赋值为函数体之前,此时fn的值是undefined,所以无法正确调用

5 - 原型和this指向

5.1对象的三种创建方式—复习

  1. 字面量方式var obj = {};
  2. new关键字var obj = new Object();
  3. 构造函数方式function Person(name,age){
    this.name = name;
    this.age = age;
    }
    var obj = new Person(‘zs’,12);

    5.2静态成员和实例成员

    5.2.1实例成员

    实例成员就是构造函数内部通过this添加的成员 如下列代码中uname age sing 就是实例成员,实例成员只能通过实例化的对象来访问
    function Star(uname, age) {
    this.uname = uname;
    this.age = age;
    this.sing = function() {
    console.log(‘我会唱歌’);
    }
    }
    var ldh = new Star(‘刘德华’, 18);
    console.log(ldh.uname);//实例成员只能通过实例化的对象来访问

    5.2.2静态成员

    静态成员 在构造函数本身上添加的成员 如下列代码中 sex 就是静态成员,静态成员只能通过构造函数来访问
    function Star(uname, age) {
    this.uname = uname;
    this.age = age;
    this.sing = function() {
    console.log(‘我会唱歌’);
    }
    }
    Star.sex = ‘男’;
    var ldh = new Star(‘刘德华’, 18);
    console.log(Star.sex);//静态成员只能通过构造函数来访问

    5.3构造函数的问题

    构造函数方法很好用,但是存在浪费内存的问题。

    5.4构造函数原型prototype

    构造函数通过原型分配的函数是所有对象所共享的。
    JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
    我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
    function Star(uname, age) {
    this.uname = uname;
    this.age = age;
    }
    Star.prototype.sing = function() {
    console.log(‘我会唱歌’);
    }
    var ldh = new Star(‘刘德华’, 18);
    var zxy = new Star(‘张学友’, 19);
    ldh.sing();//我会唱歌
    zxy.sing();//我会唱歌

    5.5对象原型

    对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。
    proto对象原型和原型对象 prototype 是等价的
    proto对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

    5.6constructor构造函数

    对象原型( proto)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。
    constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
    一般情况下,对象的方法都在构造函数的原型对象中设置。

    5.7原型链

    每一个实例对象又有一个proto属性,指向的构造函数的原型对象,构造函数的原型对象也是一个对象,也有proto属性,这样一层一层往上找就形成了原型链。

    5.8构造函数实例和原型对象三角关系

    1.构造函数的prototype属性指向了构造函数原型对象
    2.实例对象是由构造函数创建的,实例对象的proto属性指向了构造函数的原型对象
    3.构造函数的原型对象的constructor属性指向了构造函数,实例对象的原型的constructor属性也指向了构造函数

    5.9原型链和成员的查找机制

    任何对象都有原型对象,也就是prototype属性,任何原型对象也是一个对象,该对象就有proto属性,这样一层一层往上找,就形成了一条链,我们称此为原型链;
    当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
    如果没有就查找它的原型(也就是 proto指向的 prototype 原型对象)。
    如果还没有就查找原型对象的原型(Object的原型对象)。
    依此类推一直找到 Object 为止(null)。
    proto对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

    5.10原型对象中this指向

    构造函数中的this和原型对象的this,都指向我们new出来的实例对象
    function Star(uname, age) {
    this.uname = uname;
    this.age = age;
    }
    var that;
    Star.prototype.sing = function() {
    console.log(‘我会唱歌’);
    that = this;
    }
    var ldh = new Star(‘刘德华’, 18);
    // 1. 在构造函数中,里面this指向的是对象实例 ldh
    console.log(that === ldh);//true
    // 2.原型对象函数里面的this 指向的是 实例对象 ldh

    5.11通过原型为数组扩展内置方法

    Array.prototype.sum = function() {
    var sum = 0;
    for (var i = 0; i < this.length; i++) {
    sum += this[i];
    }
    return sum;
    };
    //此时数组对象中已经存在sum()方法了 可以始终 数组.sum()进行数据的求

    6 - 函数进阶

    6.1函数的定义方式

  4. 方式1 函数声明方式 function 关键字 (命名函数)function fn(){}

  5. 方式2 函数表达式(匿名函数)var fn = function(){}
  6. 方式3 new Function() var f = new Function(‘a’, ‘b’, ‘console.log(a + b)’);
    f(1, 2);

var fn = new Function(‘参数1’,’参数2’…, ‘函数体’)
注意
/Function 里面参数都必须是字符串格式
第三种方式执行效率低,也不方便书写,因此较少使用
所有函数都是 Function 的实例(对象)
函数也属于对象
/

6.2函数的调用

/ 1. 普通函数 /
function fn() {
console.log(‘人生的巅峰’);
}
fn();
/ 2. 对象的方法 /
var o = {
sayHi: function() {
console.log(‘人生的巅峰’);
}
}
o.sayHi();
/ 3. 构造函数/
function Star() {};
new Star();
/ 4. 绑定事件函数/
btn.onclick = function() {}; // 点击了按钮就可以调用这个函数
/ 5. 定时器函数/
setInterval(function() {}, 1000); 这个函数是定时器自动1秒钟调用一次
/ 6. 立即执行函数(自调用函数)/
(function() {
console.log(‘人生的巅峰’);
})();

6.3函数内部的this指向

这些 this 的指向,是当我们调用函数的时候确定的。调用方式的不同决定了this 的指向不同
一般指向我们的调用者.

6.4改变函数内部 this 指向

call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的 this 指向
应用场景: 经常做继承.
var o = {
name: ‘andy’
}
function fn(a, b) {
console.log(this);
console.log(a+b)
};
fn(1,2)// 此时的this指向的是window 运行结果为3
fn.call(o,1,2)//此时的this指向的是对象o,参数使用逗号隔开,运行结果为3
以上代码运行结果为:

6.5.高阶函数

高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。
此时fn 就是一个高阶函数
函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最典型的就是作为回调函数。
同理函数也可以作为返回值传递回来

7 - 闭包

7.1变量的作用域复习

变量根据作用域的不同分为两种:全局变量和局部变量。

  1. 函数内部可以使用全局变量。
  2. 函数外部不可以使用局部变量。
  3. 当函数执行完毕,本作用域内的局部变量会销毁。

    7.2什么是闭包

    闭包(closure)指有权访问另一个函数作用域中变量的函数。简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量。

    7.3闭包的作用

    作用:延伸变量的作用范围。
    function fn() {
    var num = 10;
    function fun() {
    console.log(num);
    }
    return fun;
    }
    var f = fn();
    f();

    7.4闭包的案例

    闭包应用-3秒钟之后,打印所有li元素的内容
    for (var i = 0; i < lis.length; i++) {
    (function(i) {
    setTimeout(function() {
    console.log(lis[i].innerHTML);
    }, 3000)
    })(i);
    }

    8 - let和const

    8.1 let

    ES6中新增了用于声明变量的关键字

    let声明的变量只在所处于的块级有效

    if (true) {
    let a = 10;
    }
    console.log(a) // a is not defined
    注意:使用let关键字声明的变量才具有块级作用域,使用var声明的变量不具备块级作用域特性。

    不存在变量提升

    console.log(a); // a is not defined
    let a = 20;

    暂时性死区

    利用let声明的变量会绑定在这个块级作用域,不会受外界的影响
    var tmp = 123;
    if (true) {
    tmp = ‘abc’;
    let tmp;
    }

    经典面试题

    var arr = [];
    for (var i = 0; i < 2; i++) {
    arr[i] = function () {
    console.log(i);
    }
    }
    arr0;
    arr1;

经典面试题图解:此题的关键点在于变量i是全局的,函数执行时输出的都是全局作用域下的i值。
let arr = [];
for (let i = 0; i < 2; i++) {
arr[i] = function () {
console.log(i);
}
}
arr0;
arr1;

经典面试题图解:此题的关键点在于每次循环都会产生一个块级作用域,每个块级作用域中的变量都是不同的,函数执行时输出的是自己上一级(循环产生的块级作用域)作用域下的i值.

小结

  • let关键字就是用来声明变量的
  • 使用let关键字声明的变量具有块级作用域
  • 在一个大括号中 使用let关键字声明的变量才具有块级作用域 var关键字是不具备这个特点的
  • 防止循环变量变成全局变量
  • 使用let关键字声明的变量没有变量提升
  • 使用let关键字声明的变量具有暂时性死区特性

    8.2 const

    声明常量,常量就是值(内存地址)不能变化的量

    具有块级作用域

    if (true) {
    const a = 10;
    }
    console.log(a) // a is not defined

    声明常量时必须赋值

    const PI; // Missing initializer in const declaration

    常量赋值后,值不能修改

    const PI = 3.14;
    PI = 100; // Assignment to constant variable.

const ary = [100, 200];
ary[0] = ‘a’;
ary[1] = ‘b’;
console.log(ary); // [‘a’, ‘b’];
ary = [‘a’, ‘b’]; // Assignment to constant variable.

小结

  • const声明的变量是一个常量
  • 既然是常量不能重新进行赋值,如果是基本数据类型,不能更改值,如果是复杂数据类型,不能更改地址值
  • 声明 const时候必须要给定值

    8.3 let、const、var 的区别

  • 使用 var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象

  • 使用 let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升
  • 使用 const 声明的是常量,在后面出现的代码中不能再修改该常量的值

    9 - 解构赋值

    ES6中允许从数组中提取值,按照对应位置,对变量赋值,对象也可以实现解构

    9.1 数组解构

    let [a, b, c] = [1, 2, 3];
    console.log(a)//1
    console.log(b)//2
    console.log(c)//3
    //如果解构不成功,变量的值为undefined

    9.2 对象解构

    let person = { name: ‘zhangsan’, age: 20 };
    let { name, age } = person;
    console.log(name); // ‘zhangsan’
    console.log(age); // 20

    let {name: myName, age: myAge} = person; // myName myAge 属于别名
    console.log(myName); // ‘zhangsan’
    console.log(myAge); // 20

9.3 小结

  • 解构赋值就是把数据结构分解,然后给变量进行赋值
  • 如果结构不成功,变量跟数值个数不匹配的时候,变量的值为undefined
  • 数组解构用中括号包裹,多个变量用逗号隔开,对象解构用花括号包裹,多个变量用逗号隔开
  • 利用解构赋值能够让我们方便的去取对象中的属性跟方法

    10 - 箭头函数

    10.1语法

    ES6中新增的定义函数的方式。
    () => {} //():代表是函数; =>:必须要的符号,指向哪一个代码块;{}:函数体
    const fn = () => {}//代表把一个函数赋值给fn
    函数体中只有一句代码,且代码的执行结果就是返回值,可以省略大括号
    function sum(num1, num2) {
    return num1 + num2;
    }
    //es6写法
    const sum = (num1, num2) => num1 + num2;

如果形参只有一个,可以省略小括号
function fn (v) {
return v;
}
//es6写法
const fn = v => v;

箭头函数不绑定this关键字,箭头函数中的this,指向的是函数定义位置的上下文this
const obj = { name: ‘张三’}
function fn () {
console.log(this);//this 指向 是obj对象
return () => {
console.log(this);//this 指向 的是箭头函数定义的位置,那么这个箭头函数定义在fn里面,而这个fn指向是的obj对象,所以这个this也指向是obj对象
}
}
const resFn = fn.call(obj);
resFn();

10.2 小结

  • 箭头函数中不绑定this,箭头函数中的this指向是它所定义的位置,可以简单理解成,定义箭头函数中的作用域的this指向谁,它就指向谁
  • 箭头函数的优点在于解决了this执行环境所造成的一些问题。比如:解决了匿名函数this指向的问题(匿名函数的执行环境具有全局性),包括setTimeout和setInterval中使用this所造成的问题

    10.3 面试题

    var age = 100;

var obj = {
age: 20,
say: () => {
alert(this.age)
}
}

obj.say();//箭头函数this指向的是被声明的作用域里面,而对象没有作用域的,所以箭头函数虽然在对象中被定义,但是this指向的是全局作用域

11 promise

11.1 为什么需要promise

像写同步代码一样写异步代码

11.2 Promise的基本使用

Promise是一个构造函数, 通过new关键字实例化对象
语法
new Promise((resolve, reject)=>{})

  • Promise接受一个函数作为参数
  • 在参数函数中接受两个参数
    • resolve: 成功函数
    • reject: 失败函数

promise实例
promise实例有两个属性

  • state: 状态
  • result: 结果

    1) promise的状态

    第一种状态: pending(准备, 待解决, 进行中)
    第二种状态: fulfilled(已完成, 成功)
    第三种状态: rejected(已拒绝, 失败)

    2) promise状态的改变

    通过调用resolve()和reject()改变当前promise对象的状态
    示例
    const p = new Promise((resolve, reject) => {
    // resolve(): 调用函数, 使当前promise对象的状态改成fulfilled
    resolve()
    })
    console.dir(p) // fulfilled
    示例
    const p = new Promise((resolve, reject) => {
    // resolve(): 调用函数, 使当前promise对象的状态改成fulfilled
    // reject(): 调用函数, 使当前promise对象的状态改成rejected

    // resolve()
    reject()
    })
    console.dir(p)

  • resolve(): 调用函数, 使当前promise对象的状态改成fulfilled

  • reject(): 调用函数, 使当前promise对象的状态改成rejected

promise状态的改变是一次性的, 单向不可逆

3) promise的结果

示例
const p = new Promise((resolve, reject)=> {
// 通过调用resolve, 传递参数, 改变 当前promise对象的 结果
resolve(‘成功的结果’)
//reject(‘失败的结果’)
})
console.dir(p)

11.3 Promise的方法

then方法

示例
const p = new Promise((resolve, reject)=> {
// 通过调用resolve, 传递参数, 改变 当前promise对象的 结果
resolve(‘成功的结果’)
//reject(‘失败的结果’)
})

// then方法函数
// 参数
// 1. 是一个函数
// 2. 还是一个函数
// 返回值: 是一个promise对象
p.then(()=>{
// 当promise的状态是fulfilled时, 执行
console.log(‘成功时调用’)
}, () => {
// 当promise的状态是rejected时, 执行
console.log(‘失败时调用’)
})
console.dir(p)
示例
const p = new Promise((resolve, reject)=> {
// 通过调用resolve, 传递参数, 改变 当前promise对象的 结果
//resolve(‘123’)
reject(‘失败的结果’)
})

// then方法函数
// 参数
// 1. 是一个函数
// 2. 还是一个函数
// 返回值: 是一个promise对象
p.then((value)=>{
// 当promise的状态是fulfilled时, 执行
console.log(‘成功时调用’, value)
}, (err) => {
// 当promise的状态是rejected时, 执行
console.log(‘失败时调用’, err)
})
console.dir(p)

  • 在then方法的参数函数中, 通过形参使用promise对象的结果

then方法返回一个新的promise实例, 状态是pending
const p = new Promise((resolve, reject)=> {
// 通过调用resolve, 传递参数, 改变 当前promise对象的 结果
resolve(‘123’)
//reject(‘失败的结果’)
})

// then方法函数
// 参数
// 1. 是一个函数
// 2. 还是一个函数
// 返回值: 是一个promise对象
const t = p.then((value)=>{
// 当promise的状态是fulfilled时, 执行
console.log(‘成功时调用’, value)
}, (reason) => {
// 当promise的状态是rejected时, 执行
console.log(‘失败时调用’, reason)
})
console.dir(t)
promise的状态不改变, 不会执行then里的方法
// 如果promise的状态不改变, then里的方法不会执行
new Promise((resolve, reject) => {

}).then((value) => {
console.log(‘成功’)
}, (reason) => {
console.log(‘失败’)
})
在then方法中, 通过return将返回的promise实例改为fulfilled状态
// 如果promise的状态不改变, then里的方法不会执行
const p = new Promise((resolve, reject) => {
resolve()
})

const t = p.then((value) => {
console.log(‘成功’)
// 使用return可以将t实例的状态改成fulfilled
return 123
}, (reason) => {
console.log(‘失败’)
})

t.then((value) => {
console.log(‘成功2’, value)
}, (reason) => {
console.log(‘失败’)
})
如果在then方法中, 出现代码错误, 会将返回的promise实例改为rejected状态
// 如果promise的状态不改变, then里的方法不会执行
const p = new Promise((resolve, reject) => {
resolve()
})

const t = p.then((value) => {
console.log(‘成功’)
// 使用return可以将t实例的状态改成fulfilled
//return 123

// 如果这里的代码出错, 会将t实例的状态改成rejected
console.log(a)

}, (reason) => {
console.log(‘失败’)
})

t.then((value) => {
console.log(‘成功2’, value)
}, (reason) => {
console.log(‘失败’, reason)
})