1

作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。

function foo() {
    var a = 1
}
foo()
console.log(a) // Uncaught ReferenceError: inVariable is not defined

上面例子可以理解为:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

在JavaScript中,作用域可以分为

  • 全局作用域
  • 函数作用域

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了‘块级作用域’,可通过新增命令let和const来体现。

块级作用域

块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

作用域链

1. 自由变量

首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量的值如何得到 —— 向父级作用域寻找(注意:这种说法并不严谨,下文会重点解释)。

2.什么是作用域链

如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链

let a = 'global';
console.log(a);

function course() {
    let b = 'zhaowa';
    console.log(b);

    session();
    function session() {
        let c = 'this';
        console.log(c);

        teacher();
        function teacher() {
            let d = 'yy';
            console.log(d);

            console.log('test1', b);
        }
    }
}
console.log('test2', b);
course();

if(true) {
    let e = 111;
    console.log(e);
}
console.log('test3', e)

执行上下文

许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。

JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:

解释阶段:

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段:

  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。

作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

this

this的绑定实际是函数被调用时才发生的绑定,也就是this指向什么,取决于你如何调用函数.

this是在执行时动态读取上下文决定的,而不是创建时

this的绑定规则

  • 默认绑定
function foo() { 
   console.log( this.a );
}
var a = 2; 
foo(); // 2

默认绑定:将全局对象绑定this

注意:在严格模式下(strict mode),全局对象将无法使用默认绑定,即执行会报undefined的错误

  • 隐式绑定

除了直接对函数进行调用外,有些情况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。

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

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); // 3
  • 隐式丢失(回调函数)
function foo() { 
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

setTimeout( obj.foo, 100 ); // 2

同样的道理,虽然参传是obj.foo,因为是引用关系,所以传参实际上传的就是foo对象本身的引用。对于setTimeout的调用,还是 setTimeout -> 获取参数中foo的引用参数 -> 执行 foo 函数,中间没有obj的参与。这里依旧进行的是默认绑定。

  • 显示绑定

callapplybind

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

var a = 2;

var obj1 = { 
    a: 3,
};

var obj2 = { 
    a: 4,
};
foo.call( obj1 ); // 3
foo.call( obj2 ); // 4

call、apply、bind的区别

callapply的用法几乎一样,唯一的不同就是传递的参数不同,call只能一个参数一个参数的传入。
apply则只支持传入一个数组,哪怕是一个参数也要是数组形式。最终调用函数时候这个数组会拆成一个个参数分别传入。

至于bind方法,他是直接改变这个函数的this指向并且返回一个新的函数,之后再次调用这个函数的时候this都是指向bind绑定的第一个参数。bind传餐方式跟call方法一致。

手写bind

Function.prototype.myBind = function() {
    const _this = this
    // 复制参数
    const args = Array.prototype.slice.call(arguments);
    const newThis = args.shift();
    
    
    return function() {
        return _this.apply(newThis, args)    
    }
}

手写apply

Function.prototype.myApply = function(context) {
    // 参数检测
    context = context || window;
    
    // 指向挂载函数
    context.fn = this;
    
    // 
}

绑定规则优先级

  1. 是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象
  2. 是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。
  3. 是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
  4. 如 果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。

规则例外

function foo() {     console.log( this.a );}foo.call( null ); // 2foo.call( undefined ); // 2

箭头函数

var foo = () => {     
    console.log( this.a );
}

var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

obj.foo(); //2
foo.call(obj); //2 ,箭头函数中显示绑定不会生效
function foo(){ 
    return () => {
        console.log( this.a );
    }    
}



var a = 2;

var obj = { 
    a: 3,
    foo: foo 
};

var bar = obj.foo();
bar(); //3

闭包

什么是闭包?

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

闭包场景

  • 函数作为返回值的场景
 function mail() {
     let content = '信';
     return function() {
         console.log(content);
     }
 }
const envelop = mail();
envelop();
  • 函数作为参数
// 单一职责
let content;
// 通用存储
function envelop(fn) {
    content = 1;

    fn();
}

// 业务逻辑
function mail() {
    console.log(content);
}

envelop(mail);
  • 函数嵌套
let counter = 0;

function outerFn() {
    function innerFn() {
        counter++;
        console.log(counter);
        // ...
    }
    return innerFn;
}
outerFn()();
  • 事件执行
let lis = document.getElementsByTagName('li');

for(var i = 0; i < lis.length; i++) {
    (function(i) {
        lis[i].onclick = function() {
            console.log(i);
        }
    })(i);
}

参考文章


看见了
876 声望16 粉丝

前端开发,略懂后台;