因一段JavaScript代码引发的闲扯

2024-05-13 20:32

本文主要是介绍因一段JavaScript代码引发的闲扯,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前两天,一朋友给我发了一段JavaScript代码:


<code class="hljs" javascript="">  function f1(){
    var n=999;
    nAdd=function(){
        n+=1
    };
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result1=f1();
    var result2=f1();
  result1(); // 999
    result2();//999
  nAdd();
   result1(); // 是999而不是1000,这是为何呢?
    result2();//1000</code>
问题的原型在这里:javascript关于闭包的面试题
这里主要利用两个知识点:声明提升 和 闭包。
在JavaScript中,没有用var关键字声明的变量均是隐式的全局变量。首先将全局变量声明提前,代码是这样子:




<code class="hljs" javascript="">  var nAdd = undefined;
  function f1(){
    var n=999;
    nAdd=function(){
        n+=1
    };
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result1=f1();
    var result2=f1();
  result1(); // 999
    result2();//999
  nAdd();
   result1(); // 是999而不是1000,这是为何呢?
    result2();//1000</code>
然后根据个人理解来解释一下原因:




<code class="hljs" cs="">var result1=f1();
var result2=f1();</code>
这里两次调用f1(),因返回的f2起到了闭包的作用,因而result1和result2都各自保存对n的引用,但是nAdd被赋值了两次,后一次赋值覆盖了前一次的值。为了便于解释,引入两个临时变量,当第一次调用f1时,代码是这样子的:


<code class="hljs" javascript=""> function f1(){
    var n=999;
    //n在result1中的引用为temp1
    var temp1 = n;
    nAdd=function(){
        temp1 += 1;
    };
    function f2(){
      alert(temp1);
    }
    return f2;
  }</code>
第二次调用f1时,代码应该是这样子的:




<code class="hljs" javascript="">function f1(){
    var n=999;
     //n在result2中的引用为temp2
    var temp2 = n;
    nAdd=function(){
        temp2 += 1;
    };
    function f2(){
      alert(temp2);
    }
    return f2;
  }</code>
所以当调用nAdd()时,只影响result2中n的值,对result1中的n没有影响。在后面再对result1和result2追加一次调用,结果应该是这样子的:




<code class="hljs" cs="">    var result1=f1();
    var result2=f1();
  result1(); // 999
    result2();//999
  nAdd();
   result1(); // 999
    result2();//1000
    result1(); // 999
    result2();//1001</code>
问题就解释到这里。如果你有其它理解,欢迎留下评论。


查找了关于声明提升和闭包的文章,除了扯一下这两点,也一并扯扯JavaScript中的回调函数、作用域和IIFEs(Immediately-Invoked Function Expressions)。


声明提升(Hoisting)
对于变量声明或函数表达式。可以看作由两部分组成:声明和赋值。JavaScript隐式地提升声明部分到封闭函数的顶部,而将赋值留在原地。需要注意的一点是:变量和函数声明存在此特征,但是函数表达式是没有这个特征的。看几段代码就知道这是怎么一回事了。


<code class="hljs" delphi="">var x= 0;
var f=function(){
    x=1;
};
f();
alert(x); 
function f(){
    x = 2;
}
f();
alert(x);</code>
猜猜上面的结果是什么?看看下面的等效代码,来校验一下你猜的答案:


<code class="hljs" delphi="">var x;
var y;
function f(){
    x = 2;
}
x = 0;
f = function(){
    x = 1;
}
f();
alert(x); 
f();
alert(x);</code>
两次弹出的结果都是1,你猜对了吗?
再举两个简单的例子来分别说明一下变量声明提升和函数声明提升。


<code class="hljs" lisp="">(function() {
  var foo = 1;
  alert(foo +   + bar +   + baz);
  var bar = 2;
  var baz = 3;
})();</code>
能猜出结果吗?看看下面的等效代码,来校验一下你猜的答案:




<code class="hljs" lisp="">(function() {
  var foo;
  var bar;
  var baz;
 
  foo = 1;
  alert(foo +   + bar +   + baz);
  bar = 2;
  baz = 3;
})();</code>
所以结果是1、undefined、undefined。
函数声明提前的好处是可以提前调用要定义的函数:




<code class="hljs" javascript="">foo();
function foo() {
  alert(Hello!);
}
//其等效的代码
function foo() {
  alert(Hello!);
}
foo();</code>
正如之前所说的,函数表达式是没有声明提前的,所以按上述方式调用会出错:




<code class="hljs" coffeescript="">foo();    //undefined is not a function
var foo = function() {
  alert(Hello!);
};</code>
闭包
闭包是JavaScript最优雅、最具表现力的特性之一。创建闭包的两种常见方式:
1、将内部函数作为值从外部函数返回




<code class="hljs" javascript="">function B()
{
    var temp=abc;
    function A()
    {
        alert(闭包函数处理本地变量temp = +temp);
    }
    return A;
    //或者直接返回
    //return functio()
    //{
    //  alert(闭包函数处理本地变量temp = +temp);
    //}
}
var a = B();
a();</code>
2、利用变量的作用范围形成闭包函数


<code class="hljs" javascript="">var F;
function B()
{
    var temp=abc;
    F=function ()
    {
        alert(利用变量范围形成闭包函数处理本地变量temp = +temp);
    }
}
B();
F();</code>
要熟练掌握闭包,就得知道关于它的三个基本事实。
事实一:JavaScript允许你引用在当前函数以外定义的变量。
ES5中可以用let关键字定义块级作用域,但在ES5之前,JavaScript没有块级作用域的概念,只有函数作用域和全局作用域(try…catch块是能形成块级作用域,异常绑定的变量只作用于catch块)。在函数外部一般无法访问函数内部定义的局部变量,但是闭包可以。(见上述代码)
事实二:即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量。




<code class="hljs" javascript="">function show(temp){
    function make(filling){
        return temp +  and  + filling;
    }
    return  make;
}
var ham = show(ham);
ham(cheese);    //ham and cheese
var turkey = show(turkey);
turkey(swiss);  //turkey and swiss</code>
尽管由相同的make函数定义,但是ham和turkey是两个完全不同的函数,都保持着各自的作用域。
事实三:闭包可以更新外部变量的值。
这一点在本文开始处已经有体现了。因为闭包存储的是外部变量的引用而不是值,所以对于任何具有访问这些外部变量的闭包,都可以更新。




<code actionscript="" class="hljs">function box(){
    var val = undefined;
    return {
        set:function(newVal){val = newVal;},
        get:function(){return val;}
        type:function(){return typeof val;}
    };
}
var b = box();
b.type();   //undefined
b.set(98.6);
b.get();  //98.6
b.type(); //number</code>
作用域
首先看一个嵌套函数的作用域问题:


<code class="hljs" javascript="">function f(){ 
    return global;
}
function test(x){
    var res = [];
    if(x){
        function f(){
            return local;
        }
        res.push(f());
    }
    res.push(f());
    return res;
}
test(true);  //?
test(false); //?</code>
如果你认为结果是[“local”,”global”]和[“global”],那么你就错了。在JavaScript中是没有块级作用域的(至少在ES 6之前,ES6将通过let关键字支持块级作用域),所以返回的结果应该[“local”,”local”]和[“local”]。但是要模拟一下块级作用域呢?接着看代码:


<code class="hljs" javascript="">function f(){ 
    return global;
}
function test(x){
    var res = [],g=f;
    if(x){
        g = function(){
            return local;
        }
        res.push(g());
    }
    res.push(g());
    return res;
}
test(true);  //[local,local]
test(false); //[global]</code>
利用var声明和函数表达式就可以模拟块级作用域效果了,但为什么第一组返回[“local”,”local”]而不是[“local”,”global”]呢?看下面的一段简单代码:


<code class="hljs" javascript="">var name = Pomy
function getName(){
    var name = dwqs;
    console.log(name);
}
getName();   //dwqs</code>
分析一下:首先在全局范围内声明了一个变量name,并赋值为Pomy;然后,声明了一个函数getName,在其内部定义一个和全局变量同名的变量name,并赋值为dwqs;最后调用函数getName,在控制台输出dwqs。


在变量作用域中,JavaScript解释器会先在当前的执行域中寻找变量name,若找到变量name,则使用变量name,停止搜寻;反之,则向上一级作用域继续搜寻变量name,一直搜寻到顶级作用域,若没有找到变量,则抛出错误。


下面的代码就能很好的解释上面的废话了:


<code class="hljs" javascript="">var locales = {
  europe: function() {          // The Europe continent's local scope
    var myFriend = Monique;
 
    var france = function() {   // The France country's local scope
      var paris = function() {  // The Paris city's local scope
        console.log(myFriend);
      };
 
      paris();
    };
 
    france();
  }
};
 
locales.europe();   // Monique;</code>
在函数paris中并没有定义myFriend变量,则在其父作用域france中搜寻,也没有定义myFriend变量,继续向上搜寻,在祖先作用域europe中找到变量,停止搜寻,输出变量的值。


这种查找方式被称为词法(静态)作用域。程序的静态结构决定了变量的作用域,而变量作用域是由其内部的代码定义的,并且嵌套函数能够接近外部作用域声明的变量。无论是以函数调用还是其它方式调用,其词法(静态)作用域取决于函数在哪里被声明。


对于JavaScript,在多层嵌套的作用中,同名变量是能被识别的,在这种情况下,局部变量优先于全局变量。如果你声明了同名的全局变量和局部变量,当在函数中使用时,局部变量会被优先使用,这种行为被称为遮蔽(shadowing)。简单地说,就是局部变量覆盖了全局变量。




<code class="hljs" javascript="">var test = I'm global;
 
function testScope() {
  var test = I'm local;
 
  console.log (test);     
}
 
testScope();           // output: I'm local
console.log(test);     // output: I'm global</code>
最后,看一道javascript-puzzlers的测试题:
test


结果是21,在JavaScript中,函数参数是绑定到arguments对象上的,因而变量的改变和arguments的改变都会映射到另一方,即使它们不在同一个作用域。


回调函数
在JavaScript中,函数是一等对象,这样造成的结果是函数可以作为参数传递给另一个函数或作为一个函数的返回值。


将函数作为参数或者返回值的函数称为高阶函数,被作为参数传递的函数称为回调函数。


回调函数常用的一个方式是当调用window对象的setTimeout或setInterval函数时—它们接收一个回调函数作为参数:




<code class="hljs" javascript="">function showMessage(message){
  setTimeout(function(){
    alert(message);
  }, 3000);  
}
showMessage('Function called 3 seconds ago');</code>
Try out the example in JS Bin
另一个示例是,当为页面上的元素添加事件监听时,需要在事件触发时提供一个回调函数:




<code class="hljs" php="">// HTML<button id="btn">Click me</button>;
 
// JavaScript
function showMessage(){
  alert('Woohoo!');
}
 
var el = document.getElementById(btn);
el.addEventListener(click, showMessage);</code>
Try out the example in JS Bin
然而理解高阶函数和回调函数最快的方式是自己去创建,因此,现在就创建一个:




<code class="hljs" javascript="">function fullName(firstName, lastName, callback){
  console.log(My name is  + firstName +   + lastName);
  callback(lastName);
}
 
var greeting = function(ln){
  console.log('Welcome Mr. ' + ln);
};
 
fullName(Jackie, Chan, greeting);</code>
Try out the example in JS Bin
回调本质上是一种设计模式,并且jQuery(包括其他框架)的设计原则遵循了这个模式。回调函数一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。


回调函数的使用场合:


资源加载:动态加载js文件后执行回调,加载iframe后执行回调,ajax操作回调,图片加载完成执行回调等等。 DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。 setTimeout的延迟时间为0,这个hack经常被用到,setTimeout调用的函数其实就是一个callback的体现 链式调用:链式调用的时候,在赋值器(setter)方法中(或者本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为你需要取值器返回你需要的数据而不是this指针,如果要实现链式方法,可以用回调函数来实现 setTimeout、setInterval的函数调用得到其返回值。由于两个函数都是异步的,即:他们的调用时序和程序的主流程是相对独立的,所以没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,否则也就失去了setTimeout及setInterval的意义了,所以用return已经没有意义,只能使用callback。callback的意义在于将timer执行的结果通知给代理函数进行及时处理。
IIFEs(Immediately-Invoked Function Expressions)
一个立即执行函数是在创建之后立即执行的函数表达式。


有两种语法稍微有点不同的方式来创建IIFEs:


<code class="hljs" javascript="">// variant 1
 
(function () {
  alert('Woohoo!');
})();
 
// variant 2
 
(function () {
  alert('Woohoo!');
}());</code>
还有很多种方式来创建IEFEs,收集的方式如下:




<code class="hljs" oxygene="">( function() {}() );
( function() {} )();
[ function() {}() ];
 
~ function() {}();
! function() {}();
+ function() {}();
- function() {}();
 
delete function() {}();
typeof function() {}();
void function() {}();
new function() {}();
new function() {};
 
var f = function() {}();
 
1, function() {}();
1 ^ function() {}();
1 > function() {}();</code>
ps:除了能提高一下逼格,基本都没什么用。


关于IEFEs,还需要知道的三件事。
1、如果你给函数分配了变量,就不需要将整个函数括放在括号里,因为它已经是一个表达式


<code class="hljs" javascript="">var sayWoohoo = function () {
  alert('Woohoo!');
}();</code>
2、IIFE末尾的分号是必须的,否则代码可能会不正常运行
3、可以给IIFE传递参数(毕竟也是一个函数),可以参考下面的示例:


<code class="hljs" lisp="">(function (name, profession) {
  console.log(My name is  + name + . I'm an  + profession + .);
})(Jackie Chan, actor);   // output: My name is Jackie Chan. I'm an actor.</code>
Try out the example in JS Bin


将全局对象作为参数传递给IIFE是很普遍的模式,因而它能调用内部函数而不依赖window对象,这样能在浏览器环境中保持代码的独立性。不管运行平台是什么,下面的代码将创建一个变量global来指向全局对象:


<code class="hljs" coffeescript="">(function (global) {
  // access the global object via 'global'
})(this);
</code>
This code will work both in the browser (where the global object is window), or in a Node.js environment (where we refer to the global object with the special variable global).


One of the great benefits of an IIFE is that, when using it, you don’t have to worry about polluting the global space with temporary variables. All the variables you define inside an IIFE will be local. Let’s check this out:


1(function(){ var today = new Date(); var currentTime = today.toLocaleTimeString(); console.log(currentTime); // output: the current local time (e.g. 7:08:52 PM) })(); console.log(currentTime); // output: undefined
Try out the example in JS Bin


是上面的示例中,第一个console.log()运行正常,而第二个console.log()运行失败,由于IIFE,today 和 currentTime成了局部变量。


我们都知道,闭包是保存外部变量的引用,而不是值的副本,因此,它返回外部变量的最新值。那么,你认为下面的输出会是什么?


<code class="hljs" javascript="">function printFruits(fruits){
  for (var i = 0; i < fruits.length; i++) {
    setTimeout( function(){
      console.log( fruits[i] );
    }, i * 1000 );
  }
}
 
printFruits([Lemon, Orange, Mango, Banana]);</code>
Try out the example in JS Bin


并不是按照我们的期望依次输出数组的四个元素,而是输出四次undefined。因为i的最新值是4,而fruits[4]是undefined。


为了修复这个问题,可以把setTimeout()放在IIFE中,并提供一个变量来保存i的副本:


<code class="hljs" javascript="">function printFruits(fruits){
  for (var i = 0; i < fruits.length; i++) {
    (function(){
      var current = i;                    // define new variable that will hold the current value of i
      setTimeout( function(){
        console.log( fruits[current] );   // this time the value of current will be different for each iteration
      }, current * 1000 );
    })();
  }
}
 
printFruits([Lemon, Orange, Mango, Banana]);</code>
Try out the example in JS Bin


另外一种方式是将i作为参数传递给IIFE:




<code class="hljs" javascript="">function printFruits(fruits){
  for (var i = 0; i < fruits.length; i++) {
    (function(current){
      setTimeout( function(){
        console.log( fruits[current] );
      }, current * 1000 );
    })( i );
  }
}
 
printFruits([Lemon, Orange, Mango, Banana]);</code>
 

这篇关于因一段JavaScript代码引发的闲扯的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/986745

相关文章

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

uniapp接入微信小程序原生代码配置方案(优化版)

uniapp项目需要把微信小程序原生语法的功能代码嵌套过来,无需把原生代码转换为uniapp,可以配置拷贝的方式集成过来 1、拷贝代码包到src目录 2、vue.config.js中配置原生代码包直接拷贝到编译目录中 3、pages.json中配置分包目录,原生入口组件的路径 4、manifest.json中配置分包,使用原生组件 5、需要把原生代码包里的页面修改成组件的方

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001

Java面试题:通过实例说明内连接、左外连接和右外连接的区别

在 SQL 中,连接(JOIN)用于在多个表之间组合行。最常用的连接类型是内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)。它们的主要区别在于它们如何处理表之间的匹配和不匹配行。下面是每种连接的详细说明和示例。 表示例 假设有两个表:Customers 和 Orders。 Customers CustomerIDCus