avaScript 变量松散类型的本质,决定了它只是在特定时间用于保存特定值的一个名字而已。由于不存在定义某个变量必须要保存何种数据类型值的规则,变量的值及其数据类型可以在脚本的生命周期内改变。
JavaScript变量分为基本数据类型和引用类型。
基本数据类型包括:Undefined、Null、Boolean、Number和String
引用数据类型的值是保存在内存中的对象
定义一个基本数据类型与引用类型的值是类似的:创建一个变量并为该变量赋值。对于引用类型的值,我们可以动态为其添加属性和方法,也可以改变和删除其属性和方法。
var person=new Object();
person.name="Nicholas";
alert(person.name); //"Nicholas"
复制代码
对于基本数据类型,不可添加或删除属性、方法,尽管这不会导致任何错误。
var name="Nicholas";
name.age=27;
alert(name.age); //undefined
复制代码
除了保存方式不同外,在从一个变量向另一个变量复制基本数据类型和引用类型值时,也存在不同。
如果从一个变量向另一个变量复制基本数据类型的值,会在变量对象上创建一个新值,然后把该值复制到新变量分配的位置上。
当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量。
ECMAScript中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。而引用类型值的传递,则如同引用类型变量的复制一样。
总之,访问变量有按值访问与按引用访问两种方式,而参数只能按值传递。
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,就是arguments对象中的一个元素)。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。
function addTen(num) {
num +=10;
return num;
}
var count=20;
var result=addTen(count);
alert(count); //20,没有变化
alert(result); //30
复制代码
对于基本数据类型的检测,可用typeof操作符,可确定一个变量是字符串、数值、布尔值,还是undefined。
var s="Nicholas";
var b=true;
var i=22;
var u;
var n=null;
var o=new Object();
alert(typeof s); //string
alert(typeof i); //number
alert(typeof b); //boolean
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof o); //object
复制代码
对于引用类型的检测,可用instanceof操作符。如果变量是给定引用类型(根据它的原型链来识别链)的实例,那么instanceof操作符就会返回true。
alert(person instanceof Object); // 变量 person 是 Object 吗?
alert(colors instanceof Array); // 变量 colors 是 Array 吗?
alert(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?
复制代码
根据规定,所有引用类型的值都是 Object 的实例。因此,在检测一个引用类型值和 Object 构造函数时,instanceof 操作符始终会返回 true。当然,如果使用 instanceof 操作符检测基本类型的值,则该操作符始终会返回false,因为基本类型不是对象。
在之前的一篇文章《前端开发过程中的HTML规范,来学习一下吧》中,我们有讲过前端开发过程中需要注意到的HTML规范问题,今天这篇文章我们继续来看下关于Javascript的规范问题。
Javascript
我们总是会在Javascript文件中定义变量,但是一不留神就会将其定义成全局变量,如果引用的外部JS文件较多,很容易出现全局变量污染的情况。
我们不推荐总是在全局空间定义变量的行为,因为所有在全局空间定义的变量都是挂在window对象上,很容易出现变量污染,如下所示。
不推荐-全局变量
IIFE就可以防止出现全局变量污染的情况,IIFE是立即执行的函数表达式,在IIFE内部会创建一个封闭的作用域,内部定义的变量不会影响外部的执行环境,而且可以通过参数传递的形式引用外部变量,最重要的一点是在函数执行完后会立即释放占用的内存。
我们推荐使用下面这种写法。
推荐写法-IIFE
为了避免全局变量的干扰,我们建议所有脚本文件都从IIFE开始。
我们都知道之所以叫立即执行的函数表达式,是因为在函数表达式后面会多一个执行的括号。这个执行的括号可以出现在两个地方,不管是在内部还是外部,都是有效的。但是为了让整个函数表达式看起来像一个整体,我们推荐将括号写在里面。
因此我们不推荐以下写法。
不推荐写法
而推荐以下写法。
推荐写法
同样,我们可以通过参数传递的形式引用外部变量。
引用外部变量
我们都知道在ES5中是没有块级作用域概念的,只有函数级作用域,而且由于变量提升的存在,在函数内部声明的变量都会提升至函数顶部,这就会造成一些难以预料的问题。
首先我们来看看变量提升是什么样的情况?看看下面一段代码。
变量提升
上面这段代码返回的结果是undefined,并不是'Hello Shenzhen',这是因为变量v会在函数内部被提升至函数顶部,实际执行的其实是下面这段代码。
实际执行
为了降低变量提升所带来的编码风险,我们应该手动声明定义的变量和方法,并把其放在函数顶部。
我们不推荐以下写法。
不推荐写法
我们推荐以下写法。
推荐写法
在编写判断相等类型的条件语句时,总是使用严格相等(===),这样可以避免Javascript在执行类型转换时带来的问题。
我们看下面一个例子,定义一个函数,传入一个数字,如果等于5,则将这个数加5返回。如果不使用严格等于,在传入一个字符串'5'后,会返回'55'。
没有使用严格相等
因此,我们推荐在使用相等判断时都采用严格相等(===)。
强烈建议在所有结束语句后面加上分号,如果不加上分号会引起一些很难发现的问题。我们看看下面一段代码。
代码
在上面这段代码执行后,我们发现即使resultOperation()函数返回-1,与-1相等,后面的method方法仍然被调用。
这是因为在上面定义的数组末尾没有加上分号,这个数组会与下面一行的-1当做表达式执行,任何非空数组-1都会返回NaN,NaN与resultOperation的返回结果-1不相等,因此后面的method方法会被执行。
省略分号不写,不只是会出现上述的问题,还有很多,这里不一一列举。
因此,建议在每个结尾的语句后加上分号,养成一个好的习惯。
闭包作为前端面试题中必不可少的知识点是应该要掌握的,而且在前端开发中经常会涉及到,关于闭包的问题,在我写的一篇文章《前端面试中不可逃避的闭包问题,你真的了解吗?》中有详细介绍,大家可以好好看下。
今天这篇文章详细的介绍了在前端开发过程中涉及到的Javascript规范问题,可能还不够全面,大家可以自行补充。
管理服务专家新钛云服 林泓辉
与许多同类语言相比,JavaScript 是一种易于学习的编程语言。但是,如果您想理解、调试和编写更好的代码,则需要多加注意一些基本概念。
在本文中,我们将了解两个这样的概念:
作为一个初学者的JavaScript,了解这些概念将有助于您了解this关键字,作用域和闭包。
一般来说,一个 JavaScript 源文件会有多行代码。作为开发人员,我们将代码组织成变量、函数、数据结构(如对象和数组)等等。
语法环境决定了我们如何以及在何处编写代码。看看下面的代码:
function doSomething() {
var age=7;
// Some more code
}
在上面的代码中,变量age在语法上位于函数内部doSomething。
请注意,我们的代码不会按原样运行。它必须由编译器翻译成计算机可理解的字节码。通常,语法环境在您的代码中会有多个。然而,并不是所有的环境都会同时执行。
帮助代码执行的环境称为执行上下文。它是当前正在运行的代码,以及有助于运行它的一切。可以有很多语法环境,但当前运行的代码只能有一个执行上下文。
查看下图以了解语法环境和执行上下文之间的区别:
语法环境与执行上下文
那么在执行上下文中到底发生了什么?代码被逐行解析,生成可执行的字节码,分配内存并执行。
让我们采用我们在上面看到的相同函数。您认为执行以下行时可能会发生什么?
var age=7;
这段源代码在最终执行之前经历了以下阶段:
下面的动画图片显示了源代码到可执行字节码的转换。
可执行字节码的源代码
所有这些事情都发生在一个执行上下文中。所以执行上下文是代码的特定部分的执行环境。
有两种类型的执行上下文:
每个执行上下文都有两个阶段:
让我们详细看看它们中的每一个,并更好地理解它们。
每当我们执行 JavaScript码时,它都会创建一个全局执行上下文(也称为基本执行上下文)。全局执行上下文有两个阶段。
在创建阶段,创建了两个独特的东西:
如果代码中声明了任何变量,则会为该变量分配内存。该变量使用唯一key进行初始化,并赋值为undefined。
如果代码中有function ,它会被直接放入内存中。我们将在Hoisting后面的部分中详细了解这部分。
代码执行在这个阶段开始。在这里进行全局变量的赋值。请注意,这里没有调用函数,因为它发生在函数执行上下文中。我们将在后面讨论这个问题。
让我们通过几个例子来理解这两个阶段。
创建一个名为index.js的空 JavaScript 文件及一个包含以下内容的 HTML 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src='./index.js'></script>
</head>
<body>
I'm loading an empty script
</body>
</html>
我们使用<script>标签将空脚本文件导入到 HTML 文件中。
在浏览器中加载 HTML 文件并打开 Chrome DevTools(快捷键通常为F12)或其他浏览器也是可以的。选择console选项卡,键入window并按回车键。您可以看到浏览器的Window对象。
windows对象
现在,输入this并按回车键。您可以看到和Window对象一样的this对象。
'this' 的值
如果您输入window===this则会得到返回值true
好的,那么我们学到了什么?
现在让我们看一个在 JavaScript 文件中包含一些代码的示例。我们将添加一个变量blog,并为其分配一个值。我们还将定义一个名为logBlog的函数。
var blog='freeCodeCamp';
function logBlog() {
console.log(this.blog);
}
在创建阶段:
在执行阶段:
当我们调用一个函数时,会创建一个函数执行上下文。让我们扩展上面使用的相同示例,但这次我们将调用该函数。
var blog='freeCodeCamp';
function logBlog() {
console.log(this.blog);
}
// Let us call the function
logBlog();
函数执行上下文经历相同的阶段,即创建和执行。
函数执行阶段可以访问一个名为arguments的特殊值。它是传递给函数的参数。但在我们的示例中,没有传递任何参数。
请注意,在全局执行上下文中创建的window对象和this变量仍然可以在此上下文中访问。
当一个函数调用另一个函数时,会为新的函数调用创建一个新的函数执行上下文。每个函数中相应的变量只能在对应的执行上下文中使用。
让我们转到另一个基本概念Hoisting。当我第一次听说Hoisting时,花了一些时间才理解这个意思。
在英语中,hoisting 的意思是使用绳索和滑轮提升某物。这可能会误导您认为 JavaScript 引擎会在特定代码执行阶段拉取变量和函数。接下来,让我们理解Hoisting的意思。
请看下面的例子并猜测输出:
console.log(name);
var name; // undefined
然而,为什么是undefined?如果我们在其他编程语言中使用类似的代码。在这种情况下,我们将在控制台得到报错,指出该变量name未声明,而我们正试图在此之前访问它。但是在JavaScript的执行上下文里:
在创建阶段,
在执行阶段,
这种为变量分配内存并赋值为undefined在执行上下文的创建阶段使用值进行初始化的机制称为Variable Hoisting(变量提升)。
特殊值undefined意味着声明了一个变量但没有赋值。
如果我们为变量分配一个这样的值:
name='freeCodeCamp';
执行阶段会将这个值赋给变量。
现在让我们谈谈Function Hoisting(函数提升)。它与Variable Hoisting的模式相同。
执行上下文的创建阶段将函数声明放入内存,并在执行阶段执行。请看下面的例子:
// Invoke the function functionA
functionA();
// Declare the function functionA
function functionA() {
console.log('Function A');
// Invoke the function FunctionB
functionB();
}
// Declare the function FunctionB
function functionB() {
console.log('Function B');
}
输出如下:
Function A
Function B
在创建阶段将整个函数声明提前放入内存称为Function Hoisting。
既然我们了解了变量提升的概念,那么让我们了解一些基本规则:
logMe();
var logMe=function() {
console.log('Logging...');
}
代码执行将中断,因为在函数初始化时,变量logMe将作为变量而不是函数被提升。因此,对于变量提升,内存分配将在初始化时发生undefined。这就是我们会得到错误的原因:
函数初始化时出错
假设我们尝试在声明之前访问一个变量,然后使用letandconst关键字来声明它。在这种情况下,它们将被提升但不会被分配默认值undefined。访问此类变量将导致ReferenceError. 下面是一个例子:
console.log(name);
let name;
它会抛出错误:
使用 let 和 const 关键字声明的提升变量时出错
如果我们使用var代替let和,相同的代码将毫无问题地运行const。这个错误是因为新的JavaScript 语言的保护机制,防止意外提升可能会导致不必要的麻烦。
感谢您能看到最后,我希望这篇文章能帮助您更好的理解JavaScript中的执行上下文与变量提升的机制。
原文:https://www.freecodecamp.org/news/javascript-execution-context-and-hoisting/
*请认真填写需求信息,我们会在24小时内与您取得联系。