对Javascript中的一些基本概念和一些难点的理解

1. 字面量

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。通俗来讲,字面量就是指这个量本身,比如字面量3。也就是指3. 再比如 string类型的字面量"ABC", 这个"ABC" 通过字来描述。 所以就是字面量,可以简单理解成一眼就能知道的量。

对比下 string x; 那么x 是多少呢? 它是个变量,你不确定它的值。 但是string x=“ABC”, 你当然知道"ABC" 就是"ABC"了,它就是一眼就能看到值的量。 string x=“ABC” 意思是把字面量"ABC" 赋值给变量x。 总之就是描述自己的量就是字面量。在这里, “ABC” 它描述了自己,所以你一眼就能知道它是"ABC"。

2. 执行上下文(Execution Contexts)

执行上下文又叫执行环境,当JS解释器开始执行代码的时候,解析器就进入一个执行环境,活动的执行环境组成一个逻辑上的栈,在这个逻辑栈顶部的执行环境是当前运行的执行环境。逻辑栈是一种特殊的数据存储格式,特点是:“先进后出,后进先出”,添加数据会先压入逻辑栈顶部,删除数据必须先从顶部开始删除。

在这里插入图片描述

JavaScript代码执行的时候,会在逻辑栈的顶部压入一个执行环境,每当遇到函数执行的时候,都会为函数开辟一个执行环境,即使对同一函数调用多次,会导致创建多个执行环境。一旦函数执行完成,执行环境将被销毁。其实这个执行环境就是我们前面所说的作用域。比如我们看下面的代码:

var a = 123;
function xx(){
    return 3;
}

function yy(){
    return 'SS';
}c
xx();
yy();
a += 6;

上面的代码的执行环境应该是这样处理的:

在这里插入图片描述

每个执行环境都有一个与之关联的变量对象,当解析器进入执行环境时,就会创建一个变量对象,变量对象保存着在当前执行环境中声明的变量和函数的引用。

变量对象是一个抽象的概念,在不同的执行环境中,变量对象有不同的身份,在解析器进入任何执行环境之前,就已经创建了一个全局对象,当解析器进入全局执行环境时,全局对象就充当变量对象,当解析器进入一个函数时,就会创建一个活动对象充当变量对象

在这里插入图片描述

2.1 变量对象(Variable Object)

每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?JavaScript为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。所以我们前面说的将某个执行环境压入到执行栈(逻辑栈),其实也可以说成将变量对象压入执行栈。

2.2 活动对象(Activation Object)

函数执行的时候,会创建一个活动对象充当其变量对象,其实活动对象也就是函数的变量对象,只是因时机和使用主体不同而起的另一个名字。

2.3 Arguments对象(Arguments Object)

当函数的执行环境被创建的时候,会创建活动对象,活动对象里面会创建Arguments对象this来进行初始化,Arguments对象被用来保存参数。

注:全局环境下并不存在Arguments对象。

3. 作用域(Scope)

作用域就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,换句话说,作用域决定了代码区块中变量和其他资源的可见性。在JavaScript中变量的作用域有全局作用域和局部作用域。JavaScript采用词法作用域(lexical scoping),也就是静态作用域。

3.1 词法(静态)作用域与动态作用域

词法(静态)作用域:词法作用域是指在词法分析阶段就确定了,不会改变。变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。

动态作用域:动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们在何处调用。

 var value = 1;
 function foo() {
   console.log(value);
 }

 function bar() {
   var value = 2;
   // 调用foo函数
   foo();
 }
 bar();  // 结果是 ???

假设JavaScript采用静态作用域,那么执行过程:

执行 foo 函数,先从 foo 函数局部作用域中查找是否有变量 value,如果没有,就从全局作用域中查找变量value的值,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

从执行结果我们可以看出,JavaScript使用的是静态作用域,也就是说,代码写完,变量的作用域已经定了(其实是全局预解析的时候最终确定),和在什么地方使用变量,没啥关系。

3.2 全局作用域、局部作用域和块级作用域

在ECMAScript 5(包括ECMAScript 5)之前的版本中,作用域只有全局作用域和局部作用域,不存在块级作用域;ECMAScript 6引入了letconst关键字,利用letconst可以形成块级作用域。

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域。全局作用域的变量是全局对象的属性。

  1. 没有用var声明的变量(函数参数除外)都具有全局作用域,是全局变量。(严格模式下,不允许该操作)
  2. 声明局部变量必须要用var,且在函数内部
  3. window的所有属性都具有全局作用域
  4. 最外层函数体外声明的变量也具有全局作用域

局部作用域

  1. 在函数内部使用var声明的变量具有局部作用域,其成为的是局部变量。
  2. 局部变量的优先级高于全局变量。
  3. 函数的形参也具有局部作用域。

块级作用域

在一个代码块(使用{}包着)中定义的所有变量在代码块的外部是不可见的,我们称这个生效区域为块级作用域。

4. 作用域链(Scope Chain)

JavaScript 中每个函数都是一个对象,函数对象有一个仅供JavaScript 引擎使用的[[scope]]属性。通过语法分析和预解析,将[[scope]] 属性指向函数定义时作用域中的所有对象集合。这个集合被称为函数的作用域链(scope chain)

我们看代码:

console.dir(xx); // f xx()
var a = 1;
function xx() {
    var b = 2;
    console.dir(yy); // f yy()
    function yy() {
        a + b;
    }
    yy();
}
xx();

通过上面的代码,我们可以看到,打印两个函数属性的时候,他们都有[[scope]]属性,这个属性里的内容就是该函数对应的作用域链。

通俗来讲就是:函数在执行的时候会先在本身的执行环境关联的变量对象中查找变量,没有的话,就会从这个函数被创建的时候的父级的执行环境关联的变量对象中去找(词法环境),一直找到全局执行环境的变量对象,这个多层的执行环境的链式关系就是函数的作用域链。

注意:函数在声明(创建)的时候,作用域链已经确定,也就是说,函数声明的时候,函数的一切都已经注定,和在哪调用无关。

作用域链延长

catch块(案例),会延长作用域链

 (function() {
     try{
         throw new Error();
     }catch(e){
         var e = 1;
         var b = 2;
         console.log(e); // 1
     }
     console.log(e); // undefined
     console.log(b); // 2
 })();

5. 闭包

对于闭包的解释,目前网上主要有三种说法:

  1. 闭包是在一个作用域中可以访问另一个函数作用域中变量的函数
  2. 当在一个作用域中访问到了另一个作用域中的变量的时候,就发生了闭包现象。
  3. 闭包是个拥有多个变量和绑定了这些变量的环境的函数。

其实在我看来三种说法描述的是同一个东西。我个人认为第一种说法更简洁,但是第二种说法更易理解。

var item = '1'
function a(){
    var item = '2'
    function b(){
        return item
    }
    return b;
}

var foo = a();
foo();

执行步骤:

  1. 开始执行,全局执行环境产生,全局执行环境内的一系列初始化
  2. 执行到a函数,创建a函数的执行环境,将该环境压入到执行逻辑栈中
  3. 在执行a函数的过程中,创建了b函数,我们知道函数在创建的时候会形成作用域链,此时b的作用域链中应该是这样的[自己的变量对象, a函数的变量对象, 全局的变量对象]
  4. a函数执行完,其执行环境被销毁,但是由于与该执行环境关联的变量对象在b函数的作用域链中有存储,这个变量对象并没有被销毁。这就形成了闭包现象,那么b函数其实就是个闭包函数。

从上面我们也可以看出,闭包产生的一个重要原因是因为作用域链早早形成。

6. this

我们前面知道,Js使用的是静态作用域,即作用域和代码定义有关,和代码执行无关。不过我们的this正好和它们相反,在js中,this的值是根据执行位置决定的,通俗来说就是this的值取决与你如何调用这个函数,而不是函数定义所在的位置。

6.1 在最外层的作用域中this指向的是window
6.2 定时器中的this指向的是window
setTimeout(function(){
    console.log(this); // 打印出一个window对象
}, 1000);

setInterval(function(){
    console.log(this); // 打印出一个window对象
}, 1000);
6.3 一般函数中使用

在一个普通的函数中,this的值在浏览器中是window。如果是在严格模式下,一般的函数调用中 this 的值是 undefined

// 'use strict'
function xxx(params) {
    console.log(this);
}
xxx();
6.4 立即执行函数中的this指向的是window
!function(){
    console.log(this); //window
}()
6.5 作为对象的方法

如果函数作为对象的方法,也就是说调用方法是对象.方法(),那么这个时候所执行的函数中this就指向这个对象本身。

// 例1

var xx = {
    name: '二愣子',
    run: function () {
        console.log(this.name); //二愣子
    }
}
xx.run(); 

// 例2
var obj = {};
function func() { 
    // console.info()可以认为是console.log()的别名
    console.info(this) 
}
obj.f = func;
obj.f(); //输出 obj, 或理解为:{f: f}
func(); //输出 window
6.6 构造函数中的this

就是在函数作为构造函数使用时,this指向的是新创建的对象。

function fn(x) {
    this.name = x;
    console.info(this); // fn {name: "小红"},和 fn {name: "小名"}
};
console.log(new fn('小红')); // fn {name: "小红"}
console.log(new fn('小名')); // fn {name: "小名"}
6.7 原型对象方法中的this

原型对象中的this,指向的也是新创建的对象。

function Cat(){
    this.run = function(){
        console.log(this.name); // 东北虎
    }
}
function Tiger(x) {
    this.name = x;
};
// 获取一个猫对象
var c1 = new Cat();
// 设置原型
Tiger.prototype = c1;

var t1 = new Tiger('东北虎');
t1.run();
6.8 DOM事件绑定的处理函数中的this

指向的是触发该事件的DOM对象。

6.9 改变this指向
6.9.1 call

call方法有两个作用:

  1. 调用函数

    function xx(){
        console.log('www.taobao.com'); // www.taobao.com
    }
    // 传统调用方式
    xx();
    // 使用call方法调用
    xx.call();
    
    function xx(age){
        console.log('www.taobao.com');
        console.log(age); //18
    }
    // 传统调用方式
    xx(18);
    // 使用call方法调用并传参
    xx.call(null, 18);
    
  2. 改变函数内部this的指向

    function xx(){
        console.log(this);
    }
    // this指向window
    xx.call(); 
    var obj = {name: '苏三'};
    // 将xx函数中的this指向改变为obj对象,苏三
    xx.call(obj); 
    
    function xx(age, sex){
        console.log(this); // {name: "张三"}
        console.log(this.name); //张三
        console.log(age); //18
        console.log(sex); //男
    }
    
    var obj = {name: '张三'};
    // 改变this指向,同时也能传参给函数
    xx.call(obj, 18, '男');
    
    

使用call实现继承:

function Cat(x, y){
    this.name = x;
    this.age =  y;
    this.run = function() {
        console.log('跑得快'); // 跑得快
    };
    this.catchMouse = function(){
        console.log('我会抓老鼠, 你会吗'); //由于JS天生异步,所以会首先打印'我会抓老鼠, 你会吗'
    }
}

function Tiger(a, b){

    this.name = '虎';
    this.run = function() {
        console.log('跑得更快, 跳得更高');
    }
    Cat.call(this, a, b);
    // Cat.apply(this, [a, b]);
}

// new 构造函数发生的过程:
// 创建一个空对象                     
// 将构造函数内部的this指向该对象        
// 执行构造函数里面的代码     
// 返回该对象

var t2 = new Tiger('大花猫', 18);
t2.catchMouse();
t2.run();
console.log(t2.name); // 大花猫
console.log(t2.age); //18
6.9.2 apply

apply方法和call方法的作用一模一样,区别只是传参的时候,传参方式不同,apply传参的时候,需要将参数写入到一个数组中

// 调用函数
function xx(age){
    console.log('www.taobao.com');
    console.log(age);
}
// 传统调用方式
xx(18);
// 使用apply方法调用, 参数放到数组中
xx.apply(null, [18]);
// 改变this指向
function xx(age, sex){
    console.log(this);
    console.log(this.name);
    console.log(age);
    console.log(sex);
}

var obj = {name: '好先生'};
// 改变this指向,同时也能传参给函数
xx.apply(obj, [18, '男']);
6.9.3 bind

bind方法和前面说的apply方法/call方法一样,可以改变this指向,不过它不会自动调用函数,不过它会返回一个经过改造后的函数的拷贝。

function xx(age, sex){
    console.log(this); // {name: "我好帅"}
    console.log(this.name); // 我好帅
    console.log(age); //18
    console.log(sex); //男
}
var obj = {name: '我好帅'};
// 这样写并不会调用函数
xx.bind(obj, 18, '男');

// 可以改变this指向
// 可以返回改变this后的函数的拷贝
// 返回值是xx函数的一个拷贝
// 这个拷贝 是个经过改造后的xx
// 改造的内容:将xx函数中this指向obj对象
// 传参方式和call一样
var X = xx.bind(obj, 18, '男');
// 手动调用
X();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值