【重学JavaScript】JavaScript标签、变量、数据类型、操作符、语句

539 阅读13分钟

1. JavaScript标签

1.1 使用script标签的两种方式

1.1.1 通过直接在网页(指html文件)中嵌入JavaScript代码来使用
<script>
    function fn() {
        console.log('楚泽')
    }
    fn()
</script>
1.1.2 通过在网页中包含外部JavaScript文件来使用
<script src='./a.js'></script>
1.1.3 注:使用了src属性的script标签内部不能再写其他的JavaScript代码,如果包含了会被忽略掉,不会被执行
<script src='./a.js'>
    // 因为script标签中存在scr属性,所以里面的JavaScript代码不会被执行
    function fn() {
        console.log('楚泽')
    }
    fn()
</script>

1.2 script标签位置

1.2.1 放在head中
<!DOCTYPE html>
<html>
    <head>
        <script src='./a.js'></script>
    </head>
    <body>
        // 页面内容
    </body>
</html>

 由于资源加载是有先后顺序的,当把JavaScript资源放在head中时,页面加载时会先将head中的资源都下载、解析完成后才能开始渲染页面,因此会导致页面的渲染延迟。为了解决这个问题,现在的页面通常将script标签资源放在body元素中的页面内容后面

1.2.2 放在body元素末尾
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
        <!-- 这里是页面内容 -->
        <script src='./a.js'></script>
    </body>
</html>

1.3 defer 和 async

defer

 告诉浏览器页面加载时立即下载脚本资源,但是推迟脚本执行,等待页面渲染完成后再执行脚本代码。且存在多个脚本时按照脚本在页面中的顺序来执行
脚本和页面的顺序:先下载脚本,然后渲染页面,最后执行脚本

<!DOCTYPE html>
<html>
    <head>
        <script src='./a.js' defer></script>
    </head>
    <body>
        <!-- 这里是页面内容 -->
    </body>
</html>
async

 同样告诉浏览器立即下载资源,和defer不同的是它下载完成后会立即执行,且存在多个script脚本时不会保证执行的顺序,所以需要确定各个脚本之间没有依赖关系。且该方式不会阻塞页面的渲染,即脚本的下载和页面是并行的

<!DOCTYPE html>
<html>
    <head>
        <script src='./a.js' async></script>
    </head>
    <body>
        <!-- 这里是页面内容 -->
    </body>
</html>

2. 变量声明

2.1 var声明变量

2.1.1 变量提升

 使用var声明的变量会成为包含它的作用域的局部变量,同时在该作用域内会存在变量提升的情况,什么是变量提升?就是说可以在声明这个变量前的地方来访问这个变量,例如:

console.log(age)   // undefined
var age = 12

 上述代码中结果打印的是undefined,而不是12,是因为var的变量提升仅仅是将变量声明提升了,而变量赋值的位置却没有变化,上述代码等同于:

var age = undefined
console.log(age)   // undefined
age = 12
2.1.2 作用域

 在变量提升的介绍中提到过var声明的变量会成为包含他的作用域(函数作用域)的局部变量,就是说var声明的变量只能提升到它所在的作用域的顶部,而不能提升到该作用域的外部,看下列代码:

function fun() {
    var age = 12
}
console.log(age)  // ReferenceError: age is not defined

当在var声明的变量的作用域外使用变量时会报错 ReferenceError: age is not defined 指age未定义

2.1.3 全局变量

 如何使用var来声明全局变量
方法一:不使用var关键字来声明,直接给变量赋值,例如:

name='楚泽'
console.log(name)  // 楚泽
function fun() {
    age=18
}
fun()
console.log(age)  // 18

方法二:在全局环境下直接使用var声明(仅在浏览器环境下使用),且只有var声明这样有效,const和let都不行

var name = '楚泽'
console.log(window.name)  // 楚泽

2.2 let声明

 上面说到var声明的变量范围是函数作用域,而接下来讲的let声明的变量是块级作用域,什么是块级作用域,就比方说小学的课桌,你在桌子上画了个三八线,三八线这边是你的,你可以使用,那边的就是你可爱的同桌的地盘了,你不能去使用,使用了就会犯错的

2.2.1 作用域
console.log(name)  // ReferenceError: Cannot access 'name' before initialization
let name = '楚泽'
console.log(name) // 楚泽

 如上述代码所示,在let声明之前使用变量的值会报错"在初始化前不能使用name",而在let声明之后是可以使用的

2.2.2 变量提升和临时性死区

 "let 与 var的另一个重要的区别,就是let声明的变量不会在作用域中被提升" ————《红宝书》
但是后面说了,"在解析代码时,JavaScript引擎也会注意出现在块后面的let声明,只不过在此之前不能以任何方式来引用未声明的变量。"
且使用一个未声明的变量以及使用一个在变量在let声明前方的报错信息如下

console.log(age)   // ReferenceError: age is not defined
console.log(name)  // ReferenceError: Cannot access 'name' before initialization
let name='楚泽'

根据报错你会发现,未定义的和已使用let定义但是提前使用的报错是不同的。
结合上述我觉得let和const是有一种形式上的变量提升,但是它们存在临时死区,就是在定义前不能引用,不然会报错

2.3 const声明

2.3.1 作用域额临时性死区

const的作用域和临时性死区是和let相同的

2.3.2 和let的不同

const声明的如果是string,number等基础数据,则会被定义为常量,常量就是不能更改的变量,在定义赋值后无法再进行修改
例外:const如果赋值的值是object类型,比如说对象类型,这是是可以修改里面的属性,例如

const obj = {name: '楚泽', age: '18'}
obj.age = 20
console.log(obj) // {name: '楚泽', age: 20}

 这里如果是新同学的话可能会觉得不是说const声明的变量无法修改么,为什么这里会修改?这就涉及到了数据的存储,普通定义的基础数据类型的变量是存在栈内存中的,这是变量名直接就指向这个值。但是像对象这些可能数据大的会存储再堆内存中,然后会分出来一个地址,声明的变量就指向这个地址,你修改变量属性时也是通过这个地址来找到对象,然后修改对象的属性,这是地址也是没有变化的,这自然就是可以的

2.4 关于面试题循环打印变量值的

面试中我们会经常遇到下面的题目:延时循环打印for语句中的内容

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, 100)
}
// 5,5,5,5,5

 这里在100毫秒后会循环打印出五个5。原因是因为var声明的变量时函数作用域,在for语句中声明时其实就相当于在外部作用中声明,之后整个语句循环完成后i的值变成了5,每个setTimeout中引用的都是同一个i,所以在打印时会连续打印出来五个5.在这里我们用最简单的解决方式就是用let

for (let i = 0; i < 5; i++) {
 setTimeout(() => {
   console.log(i)
 }, 100)
}
// 0,1,2,3,4

 这里能正常打印出来0,1,2,3,4是因为使用let声明时会存在块级作用域,每个setTimeout中引用的都是不同的i。造成这种现象的根本原因还是因为作用域

3.数据类型

3.1 typeof 操作符

  • "undefined": 表示值未定义,通常在判断未定义的变量时候会返回
  • "boolean": 表示值类型为bool值
  • "string": 表示值类型为字符串
  • "number": 表示值类型为数字类型
  • "object": 表示值类型是对象类型,包括数组和null(由于原型链最后终究会指向null,所以这边判断null时返回的是object)
  • "function": 表示值类型为方法,比如function,Date,Math等
  • "symbol": 表示值为symbol类型

3.2 undefined

 undefined是指声明了,但是没有给值(或者说值为undefined)

let str;
console.log(undefined);  // undefined

 读取一个对象数据中不存在的属性时也会返回undefined,比如

const obj = {name: '楚泽'}
const arr = [1]
console.log(obj.age);  // undefined
console.log(arr[1]);   // undefined

 在es6中有一个新特性,可以给函数参数或者对象属性结构赋值时赋予一个默认值,但这个默认值只有在该参数或者属性为undefined时才会生效,在其他比如null,0,空字符串等可以转换为false的情况下是不生效的

// 方法
function fun(name='楚泽') {
    console.log(name)
}
fun('张三');  // 张三
fun();   // 楚泽

// 对象属性
let obj = {}
const { name='楚泽' } = obj
console.log(name)  // 楚泽

3.3 null

 null类型只有一个值就是null,是声明了一个变量,且这个变量的值就是null。从逻辑上讲null是一个空对象指针,这也是为什么typeof null === 'object'

3.4 Boolean

 boolean类型是最长使用的一种类型,它只有两个值,truefalse
日常使用的值转换为boolean的方式

console.log(Boolean(0))     // false
console.log(Boolean(undefined))  // false
console.log(Boolean(null))  //false
console.log(Boolean('')) // false
console.log(Boolean(NAN)) // false
console.log(Boolean([])) // true
console.log(Boolean({})) // true
console.log(Boolean('124')) // true
console.log(Boolean(123)) // true

3.4 Number

toFixed方法,这个方法的作用是将一个数字转换成指定保留位数的字符串,数字转换完成后会变成字符串

let num = 1234.124125
num.toFixed(2)  // '1234.12'

parseInt方法,将字符串解析成整数,但是只有开头为数字的才会解析,也只能解析开头为数字且连续的那一部分。第二个参数的作用是解析的基数,默认是10进制

console.log(parseInt('123ddd')) // 123
console.log(Boolean('123.2ddd')) // 123
console.log(Boolean(124)) // 124
console.log(Boolean('asd1234')) // NaN

parseFloat方法,作用和parseInt一样,不同的是如果字符串中存在小数,他会将小数解析出来

console.log(parseInt('123.12ddd')) // 123.12

3.5 Symbol 类型

 Symbol是ES6新增的类型,符号是原始值,且实例是唯一的不可变化的。他的作用时对象使用符号作为属性的唯一标识符,保证属性不会发生冲突。

3.5.1 用法

 符号的用法是直接调用Symol,不使用new来实例化。即使Symbol中传入相同的值,他们也不相同,这个特性保证了对象的属性永远不会重合。

const sym = Symbol();
typeof sym;  // Symbol

const symb1 = Symbol('12')
const symb2 = Symbol('12')
console.log(symb1 === symb2);  // false

3.6 Object类型

 对象其实就是一组数据和功能的集合,包含的方法主要有

  • constructor:用于创建当前对象的函数
  • hasOwnProperty:用来判断改对象中是否包含某个属性,现在新特性可以使用 in 来判断(‘age’ in object)
  • isPropertyOf(object):用来判断当前对象是否是另外一个对象的原型
  • propertyIsEnumerable:用来判断给定的属性是否可用,与hasOwnProperty一样,参数必须是字符串

4. 操作符

4.1 一元操作符(递增/递减操作符)

前缀递增/递减
前缀递增/递减就是先执行递增递减操作,之后再执行其他的操作

let num = 11
// 先执行了++num操作,这是num变成了12,然后再执行num+1的操作,这是结果变成了13,并赋值给anotherNum
let anotherNum = ++num + 1
console.log(num)   // 12
console.log(anotherNum)  // 13

后缀递增/递减 后缀递增/递减操作就是先执行其他操作,之后再执行递增递减操作

let num = 11
// 先执行了num+1的操作,这时候anotherNum的结果是12,之后再执行num++的操作,之后num也变成了12
let anotherNum = num++ + 1
console.log(num)   // 12
console.log(anotherNum)  // 12

4.2 布尔操作符

4.2.1 逻辑非(!)

 逻辑非可以用于js中所有类型的值,它先将操作数转换为bool值,然后再对其取反。取反后为true的值 false、null、0、空字符串、undefined、NAN
双重的逻辑非可以把任何数值转换为对应的bool值,如:!!123(true), !!null(false)

4.2.2 逻辑与(&&)

 逻辑与可以用于任何类型的js数值,不限于bool值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,规则如下:

  • 如果第一个参数是为真值,则会返回第二个参数(有可能是执行语句,这里后面介绍尾调用优化时会介绍到)
  • 如果第一个参数是为假值,则就直接返回第一个参数,不会再执行第二个参数  逻辑与是一种短路操作,如果第一个参数决定了结果就不会去执行第二个操作数求值。如果第一个参数是为假值,就永远不会再执行第二个参数
4.2.3 逻辑或( || )

 逻辑或的执行原则是第一个执行的参数是真值的情况下就直接返回这个参数(或者语句),不会再往下执行,与逻辑与类似,同样具备短路操作特性。执行的规则如下:

  • 如果第一个参数为假值,则直接返回第二个参数;
  • 如果第一个参数为真值,则直接返回第一个参数,不会再去执行第二个参数;

4.3 乘性操作符

乘法操作符: 就是用来计算两个数值的乘积,但是有些情况下可能会导致结果为NAN,比如 'asf'*12=NaN
除法操作符号:结果是得到两数相除后的整数部分
取余: 取两数相除后的余数

4.4 指数操作符

ES6之前做数值的指数操作时是使用Math.pow()方法来做的,Math.pow(3, 2)=9,代表的是求3的2次方。
在ES7之后可以使用双乘号来表示求指数操作,3**2=9,同样是代表求3的2次方的值。

4.5 关系操作符

 关系操作符是用来比较两个值的操作,包括>, <, >=, <=,返回值是布尔值。
在比较的过程中如果类型不同时会发生类型转换的行为

  • 如果操作数都是数值,则会执行数值比较
  • 如果操作数是字符串则会逐个比较该字符对应的编码
  • 如果有一个操作数是数值,则会将另一个操作数转换为数值然后进行比较
  • 如果有一个操作数是布尔值,则会将操作数转换为数值进行比较
  • 如果有一个操作数是对象,则会调用valueOf方法,得出结果后再进行比较,如果没有valueOf方法,则会调用toString方法得出结果后再进行比较

4.6 相等操作符

全等和不全等:全等需要比较他的值和类型,是不需要做类型转换的,双等号是经过类型转换后比较他们的值是否相等,不比较类型

5. 语句

5.1 do while

 do while语句是一种后测试循环语句,指语句执行完之后再去判断是否继续执行,但这之前该语句至少执行了一次

let i = 0
do {
  i = i + 1
  console.log(i);
} while (i < 2);

5.2 while语

 while语句是一种先测试执行语句,在执行之前先判断是否需要执行,如果为false则退出循环

5.3 for, for in, for of

  • for语句仅能循环数组的数据,打印出来的元素是当前数组的索引
  • for in循环的对象是可迭代数据,包含数组和对象等数据,打印的是原始的key值,如果是数组,则打印的是索引
  • for of循环的是数组,打印的是数据的当前项

5.4 breack和continue

 continue是指立即退出当前循环,然后继续下一次循环。break则是退出当前循环,执行下一条语句

5.5 switch

 当case下面有break时,如果匹配到这一条件,则不再匹配下面的其他条件。如果没有break,如果匹配到这个条件,还会继续匹配,假如又匹配到了其他的语句则会替代该语句。default则是在所有条件都没有匹配到的情况下才会执行

switch (expression) {
  case value1:
    statement
    break;
  case value2:
    statement
    break;
  default:
    statement
}