资讯专栏INFORMATION COLUMN

You Don't Know Js 阅读笔记

wanglu1209 / 634人阅读

摘要:回调传递函数是将函数当做值并作为参数传递给函数。这个例子中就是因为事件绑定机制中的传入了回调函数,产生了闭包,引用着所在的作用域,所以此处的数据无法从内存中释放。

javascript作用域

一门语言需要一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量,这逃规则被称为作用域。

这也意味着当我们访问一个变量的时候,决定这个变量能否访问到的依据就是这个作用域。

一、词法作用域

作用域共有两种主要的工作模型,第一种是最为普通的,被大多数编程语言(包括javascript)采用的词法作用域,另一种叫做动态作用域。而我们平时所提及的作用域,就是这里所说的词法作用域

要了解词法作用域,必须要了解javascript引擎以及编译器的大概工作方式。一般程序中的源码在执行前会进行编译三步骤。

分词/语法分析

解析/语法分析

代码生成

而在分词/词法分析这个步骤,就已经确定了词法作用域。也就说作用域在我们书写代码的时候就已经确定了,引用书中的文字

词法作用域就是定义在词法阶段的作用域,换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。

具体结合编译器作用域引擎来讲,编译器在分词阶段,针对特定的环境就会生成一个词法作用域,然后对源代码中的var a = 3;类似的声明进行识别,当遇到var a,编译器会询问作用域中是否有a变量,若无,则在作用域中新增一个a变量。编译完成之后,引擎执行编译后的代码,引擎在执行的过程中遇到a变量,会去作用域中查找是否有a变量,若有,则将a赋值2。对于var a = 2;一条语句会在两个过程中操作,正是变量提升现象的原因。(稍后讲到)

那什么时候会生成一个词法作用域呢?

二、函数作用域

这幅图所展示的三个气泡,就代表了三个作用域,而编译器遇到一个函数定义,就会生成一个作用域。例如当编译器遇到foo函数,会创建一个作用域,再将这个函数内部的标识符(a/b/bar)放到词法作用域中。这个步骤在编译阶段就完成了。当js引擎执行foo函数的时候,遇到a变量,就会去询问早就创建好的作用域是否有a变量存在。

在作用域外,是无法访问作用域内的变量的。

例如

function foo() {
    var a = 3;
}
console.log(a); //undefied

正是这个特性,可以被用来实现隐藏内部变量
将重要变量声明放入一个函数声明的作用域中,可以防止被作用域外部的语句所引用甚至更改。

根据函数作用域,可以引申出如何判断一个函数是函数声明还是一个函数表达式。
最重要的区别是他们的名称标识符将会绑定在何处。

先声明一点,任何匿名函数都是可以添加名称标识符的。例如

setTimeout(function timer() {
    console.log(1)
}, 1000)

对于函数声明,名称标识符是绑定在当前作用域上的。即可在函数当前作用域调用这个名称标识符。

而函数表达式,名称标识符是绑定在自身的函数作用域中的。

按照这个区别,来看以下几个函数。

function foo1() {console.log(1)}
foo1(); // 1
var bar = function foo2() {console.log(1)}
foo2() // undefined
(function foo3() {console.log(1)})()
foo3() // undefined

以上的函数就只有foo1是函数声明。

三、块作用域

在js语言中,除了函数,创建作用域的方式还可以通过块作用域。对于js而言,循环、ifelse块并没有创建块作用域的功能。

通过ES3规范的try/catch的catch语句可以创建一个块作用域,其中声明的变量仅在catch中有效。
try-catch也正是let关键字的向前兼容方。

try {
 undefined(); // 执行一个非法操作来强制制造一个异常
} catch(err) {
    console.log(err);
}
console.log(err); // err not found

ES6引入了let关键字,提供了除var以外的另一种变量声明方式,let为其声明的变量隐式地劫持了所在的块作用域。

if (true) {
    {
        let bar = 3;
        bar = someting(bar);
        console.log(bar)
    }
}
console.log(bar) // undefined

作于的一个中括号起到划分块作用域的作用,显示的区别于var等变量。我们可能在之后会修改代码,看到这个中括号会直白的认识到这个是一个块作用域。

四、变量提升

在第一节我已经提到了,对于var a = 3;这样一条语句,编译器通过分词、解析、最后生成机器可以读的代码。

而javascript实际上会将其看成两个声明:var aa = 3。第一个声明在编译阶段进行,第二个赋值声明会留在原地等待执行。

所以在引擎工作去执行代码时,进入到函数作用域内时,首先会执行var a操作,而这个过程就好像变量从原先的位置被移动作用域最上面一样。

console.log(a); // undefined
var a = 3;

相当于

var a;
console.log(a); // undefined
a = 3;

另外函数声明也会发生变量提升的现象(连实际函数值也提升,即可以在函数声明前调用)。而行数表达式var a = function foo1() {}发生提升的是a变量,函数本身不会发生提升。

foo(); // 不是ReferenceError 而是 TypeError
var foo = function bar() {}

ReferenceError TypeError
这是两个错误标记,第一个错误标记是查询变量时,若在作用域中查找不到这个变量则发出,第二个标记是能查找到变量(即使是endefined),但是这个变量被错误的调用(比如对null,undefined进行调用),发出。

作用域闭包 一、经典的闭包

闭包是基于词法作用域书写代码时所产生的自然结果。

基于词法作用域产生的结果,这有点类似于词法作用域的产生条件。这也意味着闭包在书写代码的时候就已经形成了。

看一个最经典的闭包例子

function foo () {
    var a = 1;
    function bar () {
        console.log(a); //1
    }
    return bar;
}
var baz = foo();
baz();

基于这个经典的例子,结合书中的话

一个函数在定义时的词法作用域以外的地方被调用,可以记住并访问原先所在的词法作用域时,就产生了闭包。也即被返回出去的函数被调用时依然持有对该作用域的引用。这个引用就是闭包。

先确定一点,javascript中函数是可以作为值被传递的。基于这个特性,有多种方法可以行成闭包。只要在一个作用域中,将函数作为值传递到另一个词法作用域中并调用,就会形成闭包。

function foo() {
    var a = 2;
    function baz() {
        console.log(a);
    }
    bar(baz);
}
function bar(fn) {
    fn();
}
// 回调传递函数
var fn;
function foo() {
    var a = 2;
    function baz() {
        console.log(a);
    }
    fn = baz;
}
function bar() {
    fn();
}
foo();
bar(); //2
// 间接传递函数

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

二、回调 == 闭包

再看上一节,回调中传递函数的例子。

function foo() {
    var a = 2;
    function baz() {
        console.log(a);
    }
    bar(baz);
}
function bar(fn) {
    fn();
}
// 回调传递函数

是将函数当做值并作为参数传递给函数。再来看

function wait(message) {
    setTimeout(function timer () {
        console.log(message); // hello world
    }, 1000)
}
wait("hello world");

setTimeout作为js内置的工具函数,将timer 函数当做值传进去,在setTimeout定义函数内对传进来的timer进行了调用。类似于

function setTimeout(fn) {
    // 延迟多少毫秒
    fn();
}

回调函数timer在另一个词法作用域内调用,但是能访问原先作用域内的参数(message)。

类似jquery中的事件绑定,涉及到传递回调函数,就都有闭包的产生!

三、闭包在循环中的表现

最令人困惑的闭包表现就是在循环中了。像我们刚刚提及到的setTimeout、事件绑定等回调函数都会产生闭包。

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

这个循环的本意是想间隔1秒打印1、2、3、4、5,结果却每隔1秒输出了5次6!
结合在第二节中对setTimeout函数的解析,这个误区将很快解开。

首先要明白for循环没有块作用域的概念,即在这个循环中5次迭代都是在同一个作用域中进行的。
要清楚timer函数不是在这个作用域中被调用的,它作为参数在其他的作用域中调用。

function timer() {
    console.log(i);
}

这个函数包括其中的形式参数i原原本本的被传递,在迭代过程中i不会被赋值。
而五次迭代完成后,共用的作用域中的i的值已经变成了6 。在其他作用域中的timer函数调用过程中需要查询i,因为产生了闭包,i的值会去原始的作用域中查找,即全是6

得不到预期效果的错其实都在于for循环中共用一个作用域。想改进也很简单,即在迭代的过程中,创建对应的作用域。另外值得注意的一点是需要把每次迭代的i值传到作用域内。

for(var i = 1; i <= 5; i++) {
    (function (j) {
        setTimeout(function timer () {
            console.log(j)
        }, j* 1000)
    })(i)
}
四、闭包的垃圾回收

本来一个变量被使用完之后就可以利用垃圾回收机制进行垃圾回收,但因为闭包的产生,阻止了这一行为。

function process(data) {
    //
}
var someReallyBigData = {};
process( someReallyBigData );
var $btn = $(".j_Btn");
$btn.on("click", function clicker() {});

这个例子中就是因为事件绑定机制中的传入了clicker回调函数,产生了闭包,引用着clicker所在的作用域,所以此处的someReallyBigData数据无法从内存中释放。

解决办法也有,声明一个块作用域,让引擎清楚的知道没有必要保存someReallyBigData饿了。

function process(data) {
    //
}
{
    let someReallyBigData = {};
    process( someReallyBigData );
}
var $btn = $(".j_Btn");
$btn.on("click", function clicker() {});

阅读心得,转载请注明出处。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78518.html

相关文章

  • 读书笔记(you don&#039;t know js): this的理解(没写完...)

    摘要:基本概念首先,函数不能存储的值,指向哪里,取决于调用它的对象。如果没有这个对象,那默认就是调用非严格模式下。也就是说是在运行的时候定义的,不是在绑定的时候定义的。 基本概念 首先,函数不能存储this的值,this指向哪里,取决于调用它的对象。如果没有这个对象,那默认就是window调用(非严格模式下)。也就是说this是在运行的时候定义的,不是在绑定的时候定义的。 funct...

    freewolf 评论0 收藏0
  • You Don&#039;t Know JS阅读理解——作用域

    摘要:在我们的程序中有很多变量标识符,我们现在或者将来将使用它。当我们使用时,如果并没有找到这个变量,在非严格模式下,程序会默认帮我们在全局创建一个变量。词法作用域也就是说,变量的作用域就是他声明的时候的作用域。 作用域 定义 首先我们来想想作用域是用来干什么的。在我们的程序中有很多变量(标识符identifier),我们现在或者将来将使用它。那么多变量,我咋知道我有没有声明或者定义过他呢,...

    codeKK 评论0 收藏0
  • You don&#039;t know cross-origin

    摘要:为什么会存在跨域问题同源策略由于出于安全考虑,浏览器规定不能操作其他域下的页面,不能接受其他域下的请求不只是,引用非同域下的字体文件,还有引用非同域下的图片,也被同源策略所约束只要协议域名端口有一者不同,就被视为非同域。 showImg(https://segmentfault.com/img/remote/1460000017093859?w=1115&h=366); Why 为什么...

    hersion 评论0 收藏0
  • You Don&#039;t Know JS阅读理解——this

    摘要:运行规则根据的运作原理,我们可以看到,的值和调用栈通过哪些函数的调用运行到调用当前函数的过程以及如何被调用有关。 1. this的诞生 假设我们有一个speak函数,通过this的运行机制,当使用不同的方法调用它时,我们可以灵活的输出不同的name。 var me = {name: me}; function speak() { console.log(this.name); }...

    tianren124 评论0 收藏0
  • 一起来读you don&#039;t know javascript(一)

    摘要:一到底是一门什么样的计算机编程语言表里不一表面上是动态解释执行的脚本语言,实际上它是一门编译语言。与众不同与传统语言不同的是,它不是提前编译的,编译记过也不能在分布式系统中进行移植。千篇一律引擎进行编译的步骤和传统的编译语言非常相似。 一、JavaScript到底是一门什么样的计算机编程语言? JavaScript表里不一:表面上是动态、解释执行的脚本语言,实际上它是一门编译语言。 ...

    Anchorer 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<