变量作用域,函数提升和执行环境对象

1. 变量作用域

在 JavaScript 中,变量作用域函数 限定,即:唯一能定义变量作用域的语块就是 函数

变量 要么是全局的,要么是局部的。

  • 全局变量:在函数外部定义,处处可以访问;
  • 局部变量:在函数内部定义,只有在声明它的地方才能访问。

ES6 引入块级作用域——> let 语句

let (prisoner = 'I am in prison!') {
console.log( prisoner ); // "I am in prison!"
} console.log( prisoner ); // Erroe:prisoner is not defined"

2. 变量提升

在 JavaScript 中,当变量被声明时,声明会被提升到它所在函数的顶部,并被赋予 undefined 值。这就使得在函数的任意位置声明的变量存在于整个函数中,尽管在赋值之前,它的值一直为 undefined。

function prison () {
console.log(prisoner); // "undefined" var prisoner = 'Now I am defined!'; console.log(prisoner); // "Now I am defined!"
} prison();
  • 因为变量声明总是被提升到函数作用域的顶部,所以在函数的顶部声明变量总是最好的做法,更好的是使用单个 var 语句,这样可以和 JavaScript 的做法保持一致。

作用域和变量提升结合

[例1:]

var name = 'Joe';

function prison () {
console.log(name); // "Joe"
} prison();

[例2:变量在声明前是未定义的]

var name = 'Joe';

function prison () {
console.log(name); // "undefined"
var name; // name的声明被提升到函数的顶部,在查找全局作用域的name之前,会先检查这一被提升的声明
} prison();

[例3:变量在声明前有值]

// 变量作为参数传入
var name = 'Joe'; function prison ( name ) {
console.log(name); // "Bob"
var name; // 变量name已经由参数赋值,当声明它时,不会用undefined值覆盖。这里的声明是多余的。 console.log(name); // "Bob"
} prison( 'Bob' );

3. 高级变量提升和执行环境对象

提升

JavaScript 引擎在进入作用域时,会对代码分两轮处理:(1)初始化变量;(2)执行代码。

第一轮初始化变量,JavaScript 引擎分析代码,并做了以下3件事情:

1)声明并初始化函数参数;

2)声明局部变量,包括将匿名函数赋给一个局部变量,但并不初始化他们;

3)声明并初始化函数。

在第一轮,局部变量并未被赋值,因为可能需要在代码执行后才能确定它的值,而第一轮不会执行代码。

参数被赋值了,因为在向函数传递参数之前,任何决定参数值的代码都可以运行了。

执行环境和执行环境对象——>理解 javaScript 引擎在第一轮是如何保存变量的

JavaScript 引擎把变量作为一个属性保存在一个对象上,这个对象称为 执行环境对象

  • 执行环境:每当函数被调用的时候,就会产生一个新的执行环境,即,指函数的执行。
  • 函数声明:描述了当函数执行的时候会发生什么事情。
  • 属于执行环境部分的变量和函数,被保存在 执行环境对象 中,执行环境对象是对执行环境的 ECMA 标准实现。

执行环境 是一种概念,是运行中的函数的意思,由函数在执行时发生的所有事物组成,它不是对象。

执行环境函数声明 是分离的。

所有在函数中定义的 变量函数 都是 执行环境 的一部分。当谈论函数的 作用域 时,执行环境 也是其所指的一部分。如果变量在当前执行环境中可访问,则变量在作用域内(即:如果在函数运行时变量可访问,则该变量在作用域内)。

在 JavaScript 引擎中,执行环境对象 是一种对象,属于 JavaScript 实现层面的东西,在开发的时候无法直接访问。间接地访问执行环境对象是很容易的,因为每次使用变量,就是在访问执行环境对象的属性。

由于可以在执行环境中调用函数,会产生很多层的深度。在执行环境中调用函数,会创建一个新的嵌套在已存在的执行环境内的执行环境。

JavaScript 引擎在执行环境对象中访问作用域内的变量,查找的顺序叫作 作用域链,它和 原型链 一起,描述了 JavaScript 访问变量和属性的顺序。

 解释变量作用域链以及为什么要使用它们

作用域是很微妙的,像嵌套执行环境。更准确地讲,可以把变量作用域看作链 ——> 作用域链

在运行期,JavaScript 会检索作用域层级来解析变量名。

它从当前作用域开始,然后按它的查找方式回到顶级的作用域,即 window(浏览器)或者 global(node.js)对象。

它使用找到的第一次匹配并停止查找。

在层级更深的嵌套作用域中的变量,会使用它们的当前作用域替换更加全局的作用域,从而隐藏更加全局的作用域中的变量。

 使用原型创建 JavaScript 对象

JavaScript 对象是基于原型的,而当今其他广泛使用的语言全部都使用基于类的对象。

区别:

  • 在基于类的系统中,定义对象:使用类来描述它是什么样子的。

      类比:如果建筑是基于类的系统,则建筑师会先画出房子的蓝图,然后房子都按照蓝图来建造;
  • 在基于原型的系统中,我们创建的对象,看起来要像我们想要的所有这种类型的对象那样,然后告诉JavaScript引擎,我们想要更多像这样的对象。

      类比:如果建筑是基于原型的系统,则建筑师先建一所房子,然后将房子都建成像这种模样的。

简单对象创建:类和原型的比较

  • 基于原型的对象更简单,并且当只有一个对象实例时,编写更快,只要在适当的地方简单地定义它就行了,它也支持更复杂的使用情况,使多个对象共享相似的特性。
  • 在基于类的系统中,你得先定义类,定义构造函数,然后实例化对象,该对象是这个类的实例。

多个对象:类和原型的比较

[基于类的]:
/* step 1:定义类*/
public class Prisoner {
public int sentence = 4;
public int probation = 2;
public String name;
public String id; /* step 2:定义类的构造函数*/
public Prisoner( String name, String id ) {
this.name = name;
this.id = id;
} public static void main( String []args ) {
/* step 3:实例化对象*/
Prisoner firstPrisoner = new Prisoner("Joe", "12A"); Prisoner secondPrisoner = new Prisoner("Sam", "2BC"); System.out.println(firstPrisoner.name);
}
}
[基于原型的]:
// step 1:定义原型对象
var proto = {
sentence : 4,
probation : 2
}; // step 2:定义对象的构造函数
var Prisoner = function(name, id) {
this.name = name;
this.id = id;
} // step 3:将构造函数关联到原型
Prisoner.prototype = proto; // step 4:实例化对象
var firstPrisoner = new Prisoner('Joe', '12A');
var secondPrisoner = new Prisoner('Sam', '2BC');

补充说明:

1)在每个方法中,首先创建了 对象的模版,作为创建对象的结构。模板在基于类的编程中叫做 ,在基于原型的编程中叫作 原型对象

2)然后,创建了 构造函数。在基于类的语言中,构造函数是在类的内部定义的,这样的话,当实例化对象时,哪个构造函数与哪个类匹配,就很清晰了。在 JavaScript 中,对象的构造函数和原型是分开设置的,所以不需要额外多一步来将它们连接在一起。

3)最后,实例化对象。

JavaScript 使用了 new 操作符,违背了它基于原型的核心思想,可能是试图让熟悉基于类继承的开发人员更容易理解。用 Object.create 方法作为 new 操作符的替代方案,来创建 JavaScript 对象,能增添一种更像是基于原型的感觉。

/**
* Object.create() 方法:
* 把原型作为参数并返回一个对象,使用这种方式,可以在原型对象上定义共同的属性和方法,
* 然后使用它来创建多个共享相同属性的对象。
*/
var proto = {
sentenct : 4,
probation : 2
} var firstPrisoner = Object.create( proto );
firstPrisoner.name = 'Joe';
firstPrisoner.id = '12A'; var secondPrisoner = Object.create( proto );
secondPrisoner.name = 'Sam';
secondPrisoner.id = '2BC';

Object.create() 的改进方案:使用工厂函数来创建并返回最终的对象。

/**
* 使用 Object.create() 和工厂函数
*/
var proto = {
sentenct : 4,
probation : 2
} var makePrisoner = function( name, id ) { var prisoner = Object.create( proto );
prisoner.name = name;
prisoner.id = id; return prisoner;
} var firstPrisoner = makePrisoner( 'Joe', '12A' ); var secondPrisoner = makePrisoner( 'Sam', '2BC' );

Object.create() 方法:创建对象的最佳方法,它清晰地说明了原型是如何被设置的(有兼容性问题)。

new 操作符:创建对象的最常用方法,但是它遮掩了原型系统的细微差别。

// Cross-browser method to support Object.create()

var objectCreate = function ( arg ) {
if ( ! arg ) { return {}; }
function obj() {};
obj.prototype = arg;
return new obj;
}; Object.create = Object.create || objectCreate;

 编写自执行匿名函数

1. 函数和匿名函数:

// 声明函数
function prison () {} // 使用变量来保存函数
var prison = function prison () {}; // 用局部变量来保存的匿名函数
var prison = function () {}; // 调用方式都相同
prison();

2. 自执行匿名函数:

抛出问题:在 JavaScript 中,在全局作用域中定义的所有东西在每个地方都是可用的。有时候你不想和所有人共享,不想第三方库共享它们的内部变量,因为这很容易覆盖对象的库,从而导致难以诊断的问题。

解决方法:

  • 1)把整个程序封装在函数中,然后调用这个函数,这样外部代码就不能访问到变量了——>冗长和不灵活;

  • 2)自执行匿名函数(因为定义它时没有名字并且没有保存给变量,但却立即执行了)——>推荐。

优点:用来控制变量的作用域,阻止变量泄漏到代码的其他地方。

用途:可用于创建 JavaScript 插件,不会和应用代码冲突,因为它不会向全局名字空间添加任何变量。

[给匿名函数传参和普通函数传参对比]:
// 普通函数传参
var eatFunction = function (what_to_eat) {
var sentence = 'I am going to eat a ' + what_to_eat;
console.log( sentence );
} eatFunction( 'sandwich' ); // I am going to eat a sandwich // 自执行匿名函数传参
(function (what_to_eat) {
var sentence = 'I am going to eat a ' + what_to_eat;
console.log( sentence );
})('hotdog'); // I am going to eat a hotdog
[常用例子]:
// 保证函数作用域里面,$ 是 jQuery 对象
( function ( $ ) {
console.log( $ );
} )( jQuery );

 使用模块模式和私有变量

抛出问题:虽然我们可以把应用封装在自执行匿名函数中,使应用免受第三方库(和我们自己)的影响,但是单页应用和庞大,不能定义在一个文件中。

解决思路:将文件分成一个个的模块,每个模块都有它们自己的私有变量。

/**
* 匿名函数没有保存在 prison 变量中,因为匿名函数被执行了
* 匿名函数的返回值保存在 prison 中
*/
var prison = (function () {
var prisoner_name = 'Mike Mikowski',
jail_term = '20 year term'; return {
prisoner: prisoner_name + ' - ' + jail_term,
sentence: jail_term
};
})(); console.log( prison.prisoner_name ); // "undefined" console.log( prison.prisoner ); // "Mike Mikowski - 20 year term" console.log( prison.sentence ); // "20 year term" /**
* jail_pterm 不是 prison 对象或者原型上的属性,它是执行环境中创建的对象变量
* prison 变量保存了这个变量,并且执行环境已不复存在,因为函数已经执行结束。
*/
console.log( prison.jail_term ); // "undefined" prison.jail_term = 'Sentence commuted';
console.log( prison.jail_term ); // "Sentence commuted" console.log( prison.prisoner ); // "Mike Mikowski - 20 year term"
  • 在稍大一点的模块中,减少全局变量是很重要的。

  • 一旦自执行匿名函数停止执行,在它里面定义的变量没有了,所以它们是不能被更改的,所以它们无法通过 prison 变量访问到。它们用来定义匿名函数的返回对象上的 prison 和 sentence 属性,并且这些属性可以在 prison 变量上访问到。

  • prisoner_namejail_term 这些属性只在匿名函数执行时设置了一次,永远不会被更新。它们像是 prison 对象的私有变量,只能通过匿名函数返回的对象上的方法来访问,不能在该对象或者原型上直接访问。

  • 为了能更新“私有变量”,我们必须把属性转变为方法,每次调用它们时都会访问变量。

      var prison = (function () {
    var prisoner_name = 'Mike Mikowski',
    jail_term = '20 year term'; return {
    prisoner: function () {
    return prisoner_name + ' - ' + jail_term;
    },
    setJailTerm: function ( term ) {
    jail_term = term;
    }
    };
    })(); console.log( prison.prisoner() ); // "Mike Mikowski - 20 year term" prison.setJailTerm( 'Sentence commuted' ); console.log( prison.prisoner() ); // "Mike Mikowski - Sentence commuted"

 探索闭包的乐趣和好处

1. 什么是闭包:

JavaScript 有 垃圾回收器:当函数执行完毕时,管理内存的本地方法会将函数中所有创建了的东西从内存中移除。

闭包是阻止垃圾回收器将变量从内存中移除的方法,使得在创建变量的执行环境的外面能够访问到该变量。

var prison = (function () {
var prisoner = 'Josh Powell'; /**
* 在 prisoner 函数被保存到 prison 对象上时,一个闭包就创建了。
* 闭包因保存函数而被创建,在执行环境的外面,可以动态访问 prisoner 变量,
* 这就阻止了垃圾回收器将 prisoner 变量从内存中移除。
*/
return {
prisoner: function () {
return prisoner;
}
};
})(); prison.prisoner(); // "Josh Powell"
[闭包示例1]:
var makePrisoner = function ( prisoner ) {
return function () {
return prisoner;
}
}; var joshPrison = makePrisoner( 'Josh Powell' );
var mikePrison = makePrisoner( 'Mike Mikowski' ); console.log( joshPrison() ); // "Josh Powell"
console.log( mikePrison() ); // "Mike Mikowski"
[闭包示例2]:保存变量以便在 Ajax 请求返回时使用。
/**
* 当使用 JavaScript 对象中的方法时,this 指向这个对象。
*/
var prison = {
names: 'Mike Mikowski and Josh Powell',
who: function () {
return this.names;
}
}; prison.who(); // "Mike Mikowski and Josh Powell" /**
* 如果是 jQuery 来发送 Ajax 请求的方法,则 this 不再指向对象,它指向 Ajax 请求对象
*/
var prison = {
names: 'Mike Mikowski and Josh Powell',
who: function () {
$.ajax({
success: function () {
console.log( this.names );
}
});
}
};
// 'this' is the ajax object
prison.who(); // "undefined"

闭包由函数创建,该函数在当前执行环境中访问了某个变量,并将该函数保存给当前执行环境外的一个变量。

[闭包示例3]:通过把 this 保存给 that,在函数中访问 that,从而创建了一个闭包
var prison = {
names: 'Mike Mikowski and Josh Powell',
who: function () {
var that = this;
$.ajax({
success: function () {
console.log( that.names );
}
});
}
}; /**
* 尽管在 Ajax 请求返回的时候,who() 已经执行完毕,但是 that 变量不会被垃圾回收
* 在 success 方法中可以使用该变量
*/
prison.who(); // "Mike Mikowski and Josh Powell"

2. 闭包是如何工作的:

参看[闭包示例1],当调用 makePrison 时,为这次特定的调用创建了一个执行环境对象,将传入的值赋予 prisoner。

[闭包示例4]:
var curryLog, logHello, logGoodbye;

curryLog = function ( arg_text ) {
var log_it = function () { console.log( arg_text ); }
return log_it;
}; logHello = curryLog('hello');
logGoodbye = curryLog('goodbye'); curryLog('fred')(); // 'fred' logHello(); // 'hello'
logGoodbye(); // 'goodbye' delete window.logGoodbye; // 通过 var 声明的变量是不能通过 delete 操作符来删除的 logGoodbye(); // 'goodbye'

注意:

1)每次调用函数时都会创建一个唯一的执行环境对象。

2)执行环境对象是 JavaScript 引擎的一部分,在 JavaScript 中不能直接访问。

3)函数执行完后,执行对象就会被丢弃,除非调用者引用了它。

4)如果函数返回的是数字,就不能引用函数的执行环境对象。但是,如果函数返回的是一个更复杂的结构,像是函数、对象或者数组,将返回值保存到一个变量上(有时是误用),就创建了一个对执行环境的引用。

[闭包示例5]:
var menu, outer_function,
food = 'cake'; outer_function = function () {
var fruit, inner_function; fruit = 'apple'; inner_function = function () {
return {
food: food,
fruit: fruit
};
} return inner_function;
}; menu = outer_function(); menu(); // {food: "cake", fruit: "apple"}

解释说明:

  • 当调用 outer_function 时,创建了一个执行环境。

  • 在这个执行环境中定义了 inner_function,因为在 outer_function 执行环境里面定义了 inenr_function,它有权限访问 outer_function 作用域内的所有变量,这里是 foodfruitouter_functioninner_functionmenu

  • outer_function 执行完时,你可能期望在执行环境中的所有东西都会被垃圾回收期销毁。——>错!

  • 然而,因为 inner_function 的引用保存给了全局作用域中的变量 menu,所以它并不会被销毁。

  • 在声明 inner_function 的作用域内,需要保留对所有变量的访问权限,它“关闭”了 outer_function 执行环境的大门,阻止垃圾回收器来移除它们。

  • 这就是闭包!!!!!

[闭包示例6]:
function sendAjaxRequest() {
var scoped_var = 'yay';
$.ajax({
success: function () {
console.log(scoped_var);
}
});
} sendAjaxRequest(); // 当 Ajax 请求成功完成时,输出 'yay'

问:为什么在 Ajax 请求返回后,scope_var 还是可以访问的?

答:因为 success 方法是在调用 sendAjaxRequest 时创建的执行环境中定义的,此时 scoped_var 在作用域中。

《单页Web应用--温故JavaScrpt》学习笔记整理的更多相关文章

  1. 《MySQL必知必会》学习笔记整理

    简介 此笔记只包含<MySQL必知必会>中部分章节的整理笔记.这部分章节主要是一些在<SQL必知必会>中并未讲解的独属于 MySQL 数据库的一些特性,如正则表达式.全文本搜索 ...

  2. MySQL必知必会(第4版)整理笔记

    参考书籍: BookName:<SQL必知必会(第4版)> BookName:<Mysql必知必会(第4版)> Author: Ben Forta 说明:本书学习笔记 1.了解 ...

  3. 《SQL必知必会》学习笔记整理

    简介 本笔记目前已包含 <SQL必知必会>中的所有章节. 我在整理笔记时所考虑的是:在笔记记完后,当我需要查找某个知识点时,不需要到书中去找,只需查看笔记即可找到相关知识点.因此在整理笔记 ...

  4. 《MySQL必知必会》学习笔记——前言

    前言 MySQL已经成为世界上最受欢迎的数据库管理系统之一.无论是用在小型开发项目上,还是用来构建那些声名显赫的网站,MySQL都证明了自己是个稳定.可靠.快速.可信的系统,足以胜任任何数据存储业务的 ...

  5. 《mysql必知必会》读书笔记--存储过程的使用

    以前对mysql的认识与应用只是停留在增删改查的阶段,最近正好在学习mysql相关内容,看了一本书叫做<MySQL必知必会>,看了之后对MySQL的高级用法有了一定的了解.以下内容只当读书 ...

  6. MySQL必知必会1-20章读书笔记

    MySQL备忘 目录 目录 使用MySQL 检索数据 排序检索数据 过滤数据 数据过滤 用通配符进行过滤 用正则表达式进行搜索 创建计算字段 使用数据处理函数 数值处理函数 汇总数据 分组数据 使用子 ...

  7. 《SQL必知必会》学习笔记二)

    <SQL必知必会>学习笔记(二) 咱们接着上一篇的内容继续.这一篇主要回顾子查询,联合查询,复制表这三类内容. 上一部分基本上都是简单的Select查询,即从单个数据库表中检索数据的单条语 ...

  8. 《MySQL必知必会》整理

    目录 第1章 了解数据库 1.1 数据库基础 1.1.1 什么是数据库 1.1.2 表 1.1.3 列和数据类型 1.1.4 行 1.1.5 主键 1.2 什么是SQL 第2章 MySQL简介 2.1 ...

  9. MySQL必知必会复习笔记(1)

    MySQL必知必会笔记(一) MySQL必知必会是一本很优秀的MySQL教程书,并且相当精简,在日常中甚至能当成一本工作手册来查看.本系列笔记记录的是:1.自己记得不够牢的代码:2.自己觉得很重要的代 ...

  10. mysql学习--mysql必知必会1

     例如以下为mysql必知必会第九章開始: 正則表達式用于匹配特殊的字符集合.mysql通过where子句对正則表達式提供初步的支持. keywordregexp用来表示后面跟的东西作为正則表達式 ...

随机推荐

  1. MySQL数据库百万级高并发网站实战

    在一开始接触PHP接触MYSQL的时候就听不少人说:“MySQL就跑跑一天几十万IP的小站还可以,要是几百万IP就不行了”,原话不记得了,大体 就是这个意思.一直也没有好的机会去验证这个说法,一是从没 ...

  2. 快排找第k大模板

    int get_kth(int l,int r) { if (l==r) return a[r]; ]; while (i<j) { while (a[i]<mid) i++; while ...

  3. Android Phonebook编写联系人UI加载及联系人保存流程(三)

    2014-01-07 09:54:13  将百度空间里的东西移过来. 本文从点击“添加联系人”Button开始,分析新建联系人页面UI是如何加载,以及新的联系人信息是如何保存的,借此,我们一探Phon ...

  4. 算法导论-钢条切割 C# 递归实现

    下班前看到有位兄弟写 钢条切割问题,尝试实现C#版, 还没有实现最优版,分享一下. int[] parr; private void button1_Click(object sender, Even ...

  5. Hello Struts2

    Struts2 概述 Struts2 是一个用来开发 MVC 应用程序的框架. 它提供了 Web 应用程序开发过程中的一些常见问题的解决方案: 对来自用户的输入数据进行合法性验证; 统一的布局; 可扩 ...

  6. C++-标准输入输出

    1,cout 1) 用来向标准输出打印. 2) 如果参数是char*类型,则直接输出字符串.如果想要输出地址,则需要强制转换: <<static_cast<void*>(con ...

  7. 1、ViewModel类的构建和INoyifyPropertyChanged的应用

    public class SampleItem : INotifyPropertyChanged { public SampleItem() { } private string title; pub ...

  8. Hibernate中的一级缓存、二级缓存和懒加载

    1.为什么使用缓存 hibernate使用缓存减少对数据库的访问次数,从而提升hibernate的执行效率.hibernate中有两种类型的缓存:一级缓存和二级缓存. 2.一级缓存 Hibenate中 ...

  9. [USACO精选] 第一章 数值计算

    好不容易坑来了传说中的USACO精选,近100题我要是能做完就哈哈哈哈了…继今天学并查集连番受挫之后,决定写一写基础题. #0 负二进制 2014-01-10 其实是想到就会做,不想到就不会做的题,数 ...

  10. win10 用微软账户登录无法访问共享的问题

    百度找了一大堆可以解决的,最终最简单的方式(可能是bug): 测试了一下,Win10用微软账户登录的,连局域网共享时,输入用户名的时候,前面加个乱七八糟的域名就可以访问了: 比如: 用户名:   ba ...