JS基础(一)

目录

简介

javascript是什么

是一种运行在客户端(浏览器)的编程语言,实现人机交互效果。是一种轻量级、解释型编程语言,最初设计用于网页交互,但如今已发展为全栈语言,几乎能覆盖所有开发领域

能够做什么

  1. 网页交互与动态效果
  2. 跨平台应用开发
    web应用开发
    移动端:React Native(用 JS 写 iOS/Android 原生应用)。
  3. 后端开发(Node.js)

js的组成

1.ECMAScript
规定了js核心语法知识,如变量、数据类型、函数、对象等
2.WebAPIs
浏览器提供的API,如DOM、BOM等
DOM: 文档对象模型,用于操作网页的文档结构
BOM: 浏览器对象模型,用于操作浏览器的窗口、导航、cookie等

img

权威网站: MDN

js的书写位置

内部

写在script标签中,并且script标签一定在</body>的上方

<script>
    alert('hello world')
</script>
</body>

注意script标签的位置一定是紧贴着写在</body>标签上面,</script></body>标签之间不允许有其他内html代码
原因:
如果js下面仍有然html代码,由于浏览器按照顺序加载,所以会先加载js再加载html
js中有对html操作时,由于未加载到html,js操作失败

外部

通过<script>标签的src属性引入外部js文件,且<script>标签写在body标签中

<body>
    <script src="js/index.js">//标签之间不能写任何内容</script>
</body>

注意: script标签引入外部js文件后,script标签之间不能写任何内容,即使写了js,浏览器也会忽略

内联

代码写在元素内部,通过触发事件时执行事件对应的js代码

<button onclick="alert('hello world')">点击</button>

输出和输入

输出

输出到页面
输出到弹窗
输出到控制台

<body>
    <script>
        document.write('js输出内容到html123');
        document.write('<h1>js输出h1标签</h1>123');
        alert('浏览器告诉你');
        console.log('控制台告诉你');
    </script>
</body>

注意:
输出到页面是指把内容输出到<body>标签中,如果含有元素标签,浏览器会正常渲染,控制台查看元素可以看到输出的标签
img

prompt()函数输入

prompt("请用户输入");

img

作用:
显示一个对话框,对话框中包含提示文案和一个输入框,用户可以在输入框输入文字

prompt()函数默认输入的数据类型是string。

为什么先弹出再显示write内容

原因:
1.js是单线程的,js代码执行顺序是按照代码从上到下的顺序执行
2.alert(2) 和 console.log(2)这种不需要浏览器渲染,直接执行js就可以得到效果
3.alert会阻塞js线程,当用户点击确认弹窗消失后,dom开始渲染,页面输出write内容


变量

概念: 变量可以理解为存储数据的容器,容器中可以是数值,函数,等。

变量的基本使用

声明变量

语法: 声明关键字 变量名
声明关键字: let 等

let name;

赋值变量

语法: 声明关键字 变量名 = 值;

let name = '张三';

赋值变量的动作也叫变量初始化

声明和定义区别

声明 告诉编译器/解释器标识符的存在 声明标识符(变量、函数、类等),但可能不分配内存或实现细节。
定义 分配内存,实现细节 定义标识符(变量、函数、类等),并分配内存,实现细节。

定义的理解: 定义包含声明和赋值
例如定义一个变量a,let a = 1;
定义一个函数: function fn(){};

更新变量

把name更新为'李四'

let name = '张三';
name = '李四';

注意:
1.let 声明的变量,不能重复声明,否则会报错,即使使用var声明,也不能再试用let声明
无法重新声明块范围的变量 'name'
img

var a = 1;
let a = 2; // Identifier 'a' has already been declared

2.var 声明的变量,可以重复声明,不会报错
img

初始化多个变量

let name = '张三', age = 18, sex = '男';

img
初始化多个变量使用逗号隔开

案例-用户输入内容,展示在页面,并弹出提示框已完成

let input = prompt('请输入姓名');
document.write(input);
alert('成功');
console.log(input);

img

document.write(input);在js执行结束后,会把输入的内容输出到页面中

变量命名规则和规范

规则:
1.变量名不能以js的关键字命名
2.只能使用下划线、字母、数字、$组成,且数字不能开头
3.区分大小写, Age 和 age 是两个不同的变量

规范:
1.变量名称要有意义,看命名就能知道大概得用途和作用
2.遵守小驼峰命名,如: customerShortName 第一个单词的第一个字母小写,后面的字母的第一单词大写

let和var的区别

var:
1.不声明可以赋值,也可以先使用再声明
2.可以重复声明,会覆盖前面的值
3.变量提升,全局变量,没有块级作用域等

// 先使用变量,再声明变量为var
age = 10;
console.log(age);
var age;
// 先使用变量,不能再声明变量为let
name = 10;
console.log(name);
let name; // 这一步导致报错,错误内容: 在name=10这行代码,初始化前不能访问name

初始化前不能访问name
第17行导致报错,错误提示是name=10这行代码,初始化前不能访问name
img

let:
1.不声明可以赋值,但不能先使用,必须先声明
2.不能重复声明


is not definedcannot access before initialization错误

is not defined:变量从未声明,引擎完全找不到标识符
Cannot access before initialization:变量已声明(通过 let/const),但在初始化前被访问(TDZ 限制)


常量

使用const关键字声明一个常量,而let用于声明一个变量
使用场景: 不让变量的值改变时,可以使用const声明

const name = '张三';
name = '李四';
console.log(name);

错误提示捕获到一个错误类型: 分配常量类型的值
img
编译器报错: 常量必须赋值
img
控制台错误:
img

注意:
1.常量不允许重新赋值,赋值时会提示错误
2.声明后一定要赋值,否则会报错
3.有块级作用域,有函数作用域,不存在变量提升

var、let、const的区别

var:
1.声明可以不赋值
2.不声明可以赋值,也可以先使用再声明
3.可以重复声明,会覆盖前面的值
4.没有块级作用域,但是有函数作用域,存在变量提升

let:
1.声明可以不赋值
2.不声明可以赋值,但不能先使用再声明。否则提示初始化前不能访问
3.不能重复声明
4.有块级作用域,有函数作用域,存在变量提升,但是解释器限制了未赋值不能使用

// 代码块
{
    let a=1;
}
console.log(a); // 报错 not defined

console.log(num);
let num;

let具有变量提升,只不过js限制初始化前不能访问,因此不会报错not defined,而是报错初始化前不能访问
img


const:
1.声明后一定要赋值,否则会报错
2.常量不允许重新赋值,赋值时会提示错误

数据类型

概念: 数据类型是计算机内存中存储数据的类型

js的数据类型分为基本数据类型和引用数据类型两种:

七种基本数据类型:
null: 空类型
undefined: 未定义类型
Boolean: 布尔类型
Number: 数字类型,包括小数整数 NaN
Bigint: 任意精度的整数
String: 字符串类型
Symbol: 其实例是唯一且不可变的数据类型

一种引用数据类型:
Object: 对象类型,如数组对象函数

1.number数字类型

数字类型包括整型(Integer)单精度浮点型(Float)双精度浮点型(Double)NaNInfinity-Infinity

NaN

NaN: not a number, 它属于数字类型,代表一个计算错误。它是一个不正确或未定义数学操作结果

例如不正确的数学操作包括:
console.log(NaN + 2); // NaN
console.log(0 / 0); // NaN

对变量赋值为NaN:
let num = NaN;

特点

1.具有粘性的特性,如果一个值是NaN,那么它和任何值都是NaN,包括NaN本身
console.log(NaN + 2); // NaN

2.不等于自身

console.log(NaN === NaN); // false
console.log(NaN == NaN); // false

isNaN(): 判断一个值是否为NaN类型,true是NaN类型,false不是NaN类型

let num = '1.1';
let str = '1.1a';
console.log(isNaN(num)); // false
console.log(isNaN(str)); // true

两个NaN为什么不相等

JavaScript的数字系统基于IEEE 754浮点数标准,该标准明确规定:NaN不等于任何值,包括它自身

为了表示无效或未定义的数学运算结果(如 0/0 或 Math.sqrt(-1))的不可预测性。不同运算产生的NaN可能在逻辑上代表不同的错误,因此直接比较它们无意义。

如下代码,两个错误逻辑结果进行对比,他们的对比结果一定是false。如果两个本身无意义的逻辑进行比较返回true,会给开发人员造成错误的判断

const a = 0 / 0;       // NaN
const b = Math.sqrt(-1); // NaN
if (a === b) { /* 错误地执行逻辑 */ }

Infinity-Infinity

Infinity: 含义无限,是一个特殊的数字,它代表无穷大。
JavaScript 的数字系统基于 IEEE 754 浮点数标准,该标准规定:
1.当非零数值除以 0 时,结果为 Infinity(正无穷)或 -Infinity(负无穷)
2.这种设计是为了表示数学上“无法表示”的极大值,例如极限运算的结果

当非零数值除以 0 时,结果为 Infinity(正无穷)或 -Infinity(负无穷)。
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity

如何检测 Infinity

方式一:
数字 === Infinity
方式二:
Number.isFinite(数字): 判断数字是否是有限的

const result = 1 / 0;
console.log(result === Infinity); // true
console.log(Number.isFinite(result)); // false(Infinity 不是有限数)
console.log(Number.POSITIVE_INFINITY); 

Number.POSITIVE_INFINITY: js中Infinity全局常量

区分 0 / 0(NaN)与 1 / 0(Infinity)

0 / 0: NaN 因为数学上 0/0 是未定义的。
1 / 0: Infinity 因为它表示一个趋向无限大的极限值。

console.log(NaN === Nan); // false
console.log(Infinity === Infinity); // true

-1到1之间的小数点数字中的0可以省略

console.log(0.5 === .5); // true
console.log(-0.5 === -.5); // false

2.string字符串类型

字符串类型包括双引号单引号反引号空格换行符制表符空字符转义字符特殊字符Unicode字符

可以使用转义符输出单引号'或双引号":

let str = '我是\'单引号\'字符串';
let str = "我是\"双引号\"字符串";

img

字符串的+运算符

console.log('加号相连' + 1); // 输出:加号相连1
console.log('减号未定义的运算' - 1); // 输出:NaN

模板字符串

let age = 19;
console.log(`我的年龄是${age}`);

在使用模板字符串时需要用反引号,而不是双引号或单引号


3.boolean类型

boolean类型的值: truefalse


4.undefined类型

undefined: undefined表示一个未定义的值,它属于undefined类型
undefined类型的值只有一个undefined值,例如: let a = undefined;

let a;
console.log(a); // undefined
document.write(`a变量未赋值:${a}`);

document.write(a);页面输出: a变量未赋值:undefined
img


5.null类型

null: null表示一个空值特殊关键字,并不是空字符串,它属于null类型
null类型的值只有一个null
一般尚未创建的对象赋值为null。例如:
let userInfo = null;userInfo是一个空对象,后续会对该对象进行添加属性值。

let a = null;
console.log(a); // null

null和undefined的区别

1.null表示变量赋值了,但是这个值是null
2.undefined表示变量没有被赋值,这个值是undefined
3.是两个不同的数据类型
4.运算的区别

let name;
console.log(name + 1); // 输出:NaN
let name2 = null;
console.log(name2 + 1); // 输出:1

6.symbol类型

symbol是一种原始数据类型。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法

Symbol("foo") === Symbol("foo"); // false

var sym = new Symbol(); // TypeError
如 new Boolean、new String以及new Number,因为遗留原因仍可被创建


7.bigint类型

BigInt 是一种数字数据类型,它可以表示任意精度格式的整数。


8.object对象类型

在 JavaScript 中,对象可以看作是属性的集合。使用对象字面量语法,可以初始化一组有限的属性,然后可以添加和删除属性。属性值可以是任何类型的值


typeof关键字检查数据类型

语法:
第一种: typeof 变量名(推荐)
第二种: typeof(变量名)

console.log(typeof 1); // 输出:number
console.log(typeof '1');  // 输出:string
console.log(typeof true);  // 输出:boolean
console.log(typeof undefined);  // 输出:undefined
console.log(typeof null);  // 输出:object (这是一个已知的 JavaScript bug)
console.log(typeof {});  // 输出:object
console.log(typeof 1n);  // 输出:bigint
console.log(typeof Symbol());  // 输出:symbol
const test = function(){}
class test2 {}
console.log(typeof test); // 输出: function
console.log(typeof test2); // 输出: function

typeof 函数/类: 返回function
原因: JS的设计bug

总结

1.原始数据类型返回对应的类型,Symbol()也属于原始类型
2.引用数据类型返回object,但函数/类返回function
3.只有 undefined、boolean、number、string、function、bigint、symbol 几种类型单独返回,剩下的全是 object,包括正则表示对象也是object


既然null是一种数据类型,为什么typeof null返回了object

typeof null;返回值为object
img

简单说这是js的bug。
在Javascript中,不同的数据类型在底层都表示为二进制,比如:

000 - 对象,数据是对象的引用  
1 - 整型,数据是31位带符号整数  
010 - 双精度类型,数据是双精度数字  
100 - 字符串,数据是字符串  
110 - 布尔类型,数据是布尔值 

二进制的前三位为0会被 typeof 判定为object类型。
null是一个空值,其二进制表示全是0,自然前三位也是000,所以执行typeof的时候会返回object,产生假象。


运算符

算术运算符: + - * / % **

% 取余运算符,取余数
img


+:
1.如果两个操作数都是数字,则执行加法运算。
2.如果两个操作数都是字符串,则执行连接操作。
3.如果其中一个操作数是字符串,则将另一个操作数转换为字符串,再执行连接操作。
4.+后面是字符串,隐式转为数字类型
console.log(+'123'); // 输出:数字 123


**: 是幂运算符,表示乘方运算。
console.log(2 ** 3); // 输出:8


案例-用户画一个圆,并自动计算圆的面积

<style>
    .radius {
        border-radius: 50%;
        background-image: radial-gradient(white, grey);
    }
</style>

<div class="radius"></div>
<span></span>
<script>
    let radius = prompt('请输入元素的半径');
    document.querySelector('.radius').style.width = radius * 2 + 'px';
    document.querySelector('.radius').style.height = radius * 2 + 'px';
    document.querySelector('span').innerHTML = '圆的面积是: ' + (Math.PI * radius * radius).toFixed(2);
</script>

img


类型转换

prompt()函数返回的是字符串类型,需要转换为数字类型

let num1 = prompt('请输入数字');
let num2 = prompt('请输入数字');
console.log(num1 + num2);

img

因此需要数据类型转为数字类型才能相加

隐式转换

规则:
1.+两边只要有一个是字符串类型,就会把另一边都转为字符串再拼接

let str = "我是字符串";
console.log(str + 1); // 输出:我是字符串1

2.+后面的字符串类型的数值会被转为数值类型

console.log(typeof +'123'); // 输出:number
console.log(+'a') // NaN 无效计算
console.log(typeof +'a'); // 输出:number  NaN也是数字类型

3.除了+以外的运算符,如 - * / % 都会把数据转为数字类型

console.log(+true); // 1
console.log(-'1'); // 输出: -1
console.log(2 * '2'); // 输出: 4
console.log(2 % '1'); // 输出: 0
console.log(2 / '2'); // 输出: 1

注意:
+作为正号解析可以把后面数字字符串转化为数字类型
console.log(+'123'); // 输出: 数字 123


显示转换

JavaScript内置构造函数是语言本身提供的一些特殊函数,用于创建特定类型的对象,常见的内置构造函数包括 Object、Array、String、Number、Boolean、Date、RegExp 等

1.Number(): 将一个值转换为数字类型。

console.log(Number('1')); // 输出: 1
console.log(Number('a')); // 输出: NaN

2.String(): 将一个值转换为字符串类型。

console.log(String(123)); // 输出: "123"
console.log(String(true)); // 输出: "true"

3.Boolean(): 将一个值转换为布尔类型。

console.log(Boolean(123)); // 输出: true
console.log(Boolean('123')); // 输出: true
console.log(Boolean('0')); // 输出: true
console.log(Boolean('')); // 输出: false
console.log(Boolean(0)); // 输出: false
console.log(Boolean(1)); // 输出: true
console.log(Boolean(undefined)); // 输出: false
console.log(Boolean(null)); // 输出: false
console.log(Boolean(Infinity)); // 输出: true
console.log(Boolean(-Infinity)); // 输出: true

总结:
所有表示空含义的类型都是false,如NaNnull''undefined0
其余都是true

4.parseInt(): 解析一个以数字开头的字符串并返回一个数字类型的整数值

console.log(parseInt('a123')); // 输出 NaN
console.log(parseInt('123a1')); // 输出 123 
console.log(parseInt('123.2a')); // 输出 123 

5.parseFloat(): 解析一个以数字或.开头的字符串并返回一个数字类型的浮点数值

console.log(parseFloat('123.6a')); // 输出: 123.6
console.log(parseFloat('.123a')); // 输出: 0.123

案例-用户输入两个数字计算和

let numFirst = prompt('请输入第一个数字');
let numSecond = prompt('请输入第二个数字');
let sum = +Number(numFirst) + +numSecond;
let sum2 = Number(numFirst) + Number(numSecond);
document.write(`计算: ${sum}`);
document.write('<br>');
document.write(`计算: ${sum2}`);

更加简介写法:

let numFirst = +prompt('请输入第一个数字');
let numSecond = +prompt('请输入第二个数字');
document.write(`计算: ${numFirst + numSecond}`);

img


各种转换方式方式和转换结果

各类型转换方式

转换为字符串类型:
显示: String(变量名) 隐式: 变量名 + ''

转换为数字类型:
显示: Number(变量名) 隐式: +变量名

转换为boolean:
显示: Boolean(变量名) 隐式: !!变量名

各类型转换结果

各类型转为数字类型结果:

console.log(Number(true)); // 1
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(Number('a')); // NaN

各类型转为booelan类型结果:
所有表示空含义的类型都是false,如NaNnull''undefined0
其余都是true


案例-用户输入订单信息页面计算结果

<style>
    table,
    thead,
    tr,
    th,
    td {
        border: 1px solid black;

    }
    th,
    td {
        text-align: center;
        width: 90px;
    }
    table {
        border-collapse: collapse;
    }
</style>
<table>
    <caption>用户订单</caption>
    <thead>
        <tr>
            <th>用户姓名</th>
            <th>商品</th>
            <th>单价</th>
            <th>数量</th>
            <th>总价</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td id="customer-name">1</td>
            <td id="product-name"></td>
            <td id="price"></td>
            <td id="num"></td>
            <td id="total"></td>
        </tr>
    </tbody>
</table>
<script>
    let customerName = prompt('请输入姓名');
    let productName = prompt('请输入商品');
    let price = +prompt('请输入单价');
    let num = +prompt('请输入购买数量');
    let total = num * price;
    document.getElementById('customer-name').innerHTML = customerName;
    document.getElementById('product-name').innerHTML = productName;
    document.getElementById('price').innerHTML = price + '元';
    document.getElementById('num').innerHTML = num;
    document.getElementById('total').innerHTML = total + '元';
</script>

img


赋值运算符

=: 把值赋值给变量

+=加法赋值:
x += y 含义 x = x + y

-=减法赋值:
x -= y 含义 x = x - y

*=乘法赋值:
x *= y 含义 x = x * y

/=除法赋值:
x /= y 含义 x = x / y

%=求余赋值:
x %= y 含义 x = x % y

参考: 运算符


自增和自减运算符

++i: 前置自增,变量i会加1,返回自增后的值
i++: 后置自增,变量i会加1,返回自增前的值

相同点: 都会把i + 1,然后重新赋值给i
不同点:
前置自增,返回自增后的值
后置自增,返回自增前的值

let i = 1;
let a = ++i; // 返回新值
console.log(a); // 输出: 2
let y = 1;
let b = y++; // 返回旧值
console.log(b); // 输出: 1
let i = 1;
/*
        返回值    i的值
i++     1       2
++i     3       3
i       3       3
*/
console.log(i++ + ++i + i); // 输出: 7

自减: 使用后会把原来的值减-1
--i: 把i减1,返回i的新值
i--: 把i减1,返回i的旧值

逻辑运算符

逻辑非

!: 逻辑非,对一个boolean值取反
如果对一个非boolean值取反,会先把值转为boolean类型,然后再取反
!!: 逻辑非后再逻辑非,对一个boolean值取反后再取反
使用场景: 判断一个非boolean类型转为boolean值

!!a: 先把a转为boolean类型,然后取反,取反后的boolean值不准确了,然后再次取反,转为正确的boolean值

let a = 1;
console.log(!!a); // true

逻辑与

&&: 逻辑与,如果两个值都为true,返回true,否则返回false

true && alert(1): 会执行alert(1)
false && alert(1): 不会执行alert(1)

逻辑与是找false的运算

也就是说,&&运算如果先找到了false,则立即返回,否则继续执行后面的表达式找false

对于与非boolean值进行逻辑与运算,找到false,最终会返回false对应的原值或最后一个true对应的原值

对于非boolean类型的值进行逻辑与运算,首先把值转为boolean类型,如果是false,则返回对应的原值

console.log(1 && 2); // 2 两个都是true,返回最后一个原值
console.log(0 && 1); // 0
console.log(1 && 0); // 0
console.log(1 && NaN); // NaN

逻辑或

||: 逻辑或,当||左右两边只要有一个是true时,返回true,否则返回false

特点:

逻辑或是找true的运算

如果第一个值是false,会继续运算后面的逻辑

执行alert(1),并返回undefined

let a1 =  false || alert(1);
console.log('a1:' + a1); // 输出: undefined

alert() 函数的返回值始终是 undefined(所有浏览器内置弹窗函数均无返回值),因此上面代码返回了原值undefined

对于与非boolean值进行逻辑或运算,找到true,最终会返回true对应的原值或最后一个false对应的原值

对于非boolean类型的值进行逻辑或运算,首先把值转为boolean类型,如果是true,则返回对应的原值

console.log(1 || 2); // 1
console.log(0 || 1); // 1
console.log(1 || 0); // 1
console.log(1 || NaN); // 1

关系运算符

<>

1.当与非数字类型进行大于小于关系运算时,会先把值转为number类型,然后再进行比较

console.log('12' > 2); // true

2.当关系运算符的两端是字符串时,不会进行类型转换,会直接逐步进行比较两个字符串的unicode编码

console.log('a' < 'b'); // true
console.log('abc' < 'b'); // true
console.log('12' < '2'); // true
console.log(+'12' < +'2'); // false
console.log('12' < '2'); // true

==

相等,如果两个值相等,返回true,否则返回false。类型不同先转为同一类型(一般转为数字类型)再比较值,会进行类型转换

1.使用相等运算符比较两个不同类型的值时,通常情况下会都转化为数字类型,然后再比较

console.log('12' == 12); // true
console.log('a' == 97); // false 字符串a不能解析为有效数字,因此是NaN,NaN不等于97,因此返回false
console.log(true == '1'); // true

2.null == undefined返回true,null == 0返回false,null == false返回false,null >= 0返回true

console.log(null == undefined); // true
console.log(false == 0); // true
console.log(null == false); // false
console.log(null == null); // true
console.log(null == 0); // false
console.log(null > 0); // false
console.log(null < 0); // false
console.log(null >= 0); // true

ECMAScript规范中定了ES11, 7.2.14
1.nullundefined比较时不会转换,直接返回true。因此不会尝试将null转为0undefined转为NaN
2.null与非undefined的值(例如数字、字符串、对象等),直接返回false
img
null == 0在上面12步中,最终会走到第12步,返回false

null >= 0// true
javascript 是这么定义大于等于判断的,如果a<b false 那么 a>=b true

3.NaN和任何类型不相等,包括和自身相比也不相等

console.log(NaN == NaN); // false
console.log(NaN == 0); // false

===

全等运算符,先比较类型,然后再比较值,不会有类型转换,如果两个类型不相等直接返回false

console.log(null === undefined); // false

!=

不等运算符,用来检测两个值是否相等,会进行类型转换
console.log(false != 0);// false


!==

不全等,用来检测类型和值是否不相等,不会进行类型转换

console.log(false !== 0); // true


条件运算符

格式: 条件表达式 ? 表达式1 : 表达式2

运算符优先级

&&优先级大于||

代码块

使用 {代码}对代码分组

特点

一个代码块中的代码要么都执行,要么都不执行

{
    var a = 10;
    let b = 11;
}
console.log(a); // 输出: 10
console.log(b); // 报错: ReferenceError: b is not defined

使用场景: 封装代码,避免变量污染

function a() {
    代码
}

for (let i = 0; i < 10; i++) {
    代码
}

switch语句

语法:

switch (表达式) {
    case 表达式: 
        代码
    case 表达式: 
        代码
    case 表达式:
        代码
    default:
        代码
}

运行逻辑:
1.当switch表达式的值和case表达式的值相等,执行对应的case后面的代码
2.如果case中没有break,会继续执行下一个case,一直把default执行完毕
3.如果case中有break,会跳出switch语句,也不会执行后面的default语句
4.如果switch表达式的值和case表达式的值都不相等时,默认会执行default语句,然后跳出switch语句

let num = 1;
switch (num) {
    case 1:
        console.log('1');
    case 2:
        console.log('2');
    case 3:
        console.log('3');
    default:
        console.log('default');
}

打印:

1
2
3
default

braak: 跳出switch语句,不会执行后面的case,已经default
default: 当前switch条件没有匹配到case条件时,默认执行default语句,default语句可以不写


循环语句

while (true) {
    // 代码
}
do {
    // 代码
} while (true);
for (初始表达式; 条件表达式; 更新表达式) {
    // 代码
}

for循环执行流程:
1.先执行初始表达式
2.判断条件表达式是否为true
3.如果为true
1.执行循环体中的代码
2.然后执行更新表达式
3.循环从第2步开始执行
4.如果为false,结束循环

for特殊写法

死循环

for (;;) {
    // 代码
}

使用var声明初始条件,可以在循环体外面使用该变量,而使用let声明,则不能

for (var i = 0; i < 10; i++) {
    // 代码
}
console.log(i); // 输出: 10
for (let i = 0; i < 10; i++) {
    // 代码
}
console.log(i); // 输出: 报错: ReferenceError: i is not defined

break和continue

break: 终止最近的循环或者switch
continue: 跳过最近的循环进行下一次循环


练习_水仙花数求和

// 水仙花数: 一个n位数,每个位上的数字的n次方之和等于这个数本身
let num = 1000;
for (let i = 10; i <= num; i++) {
    let n = i.toString().length;
    let sum = 0;
    for (let y = 0; y < n; y++) {
        sum += i.toString().slice(y, y + 1) ** n;
    }
    if (sum === i) {
        console.log(i); // 输出: 153 370 371 407
    }
}

练习_判断一个数是不是质数

// 判断一个数是否是质数: 只能被1和自己整除
let num = +prompt('请输入一个数字');
let count = 0;
for (let i = 1; i <= num; i++) {
    if (num % i === 0) {
        count++;
    }
}
if (count === 2) {
    alert(`${num}是质数`);
} else {
    alert(`${num}不是质数`);
}

练习_统计100000以内的质数个数以及耗时

console.time('质数');
let num = 100000;
let count = 0;
for (let i = 2; i < num; i++) {
    let flag = true;
    for (let j = 2; j < i; j++) {
        if (i % j === 0) {
            flag = false;
            break;
        }
    }
    if (flag) {
        count++;
    }
}
console.timeEnd('质数');
console.log(`${num}以内质数总数:${count}`);

输出:

质数: 1526.02001953125 ms
100000以内质数总数:9592

优化

/*
比如11,传统做法两个因数相乘是否等于11
2 * 2
2 * 3
2 * 4
2 * 5
3 * 2
3 * 3
4 * 2
5 * 2
其中 2 * 3 和 3 * 2 重复了,随着判断数值增加,重复也会增加,因此效率不高
优化: i值增加到根号11的时候,就不需要再判断了
*/

计算一个数的平方根: Math.sqrt(num)或者num ** 0.5,都精确到小数

console.log(Math.sqrt(11 ** .5)); // 1.8211602868378718
console.log(Math.sqrt(Math.sqrt(11))); // 1.8211602868378718

优化后的代码:

console.time('质数');
let num = 100000;
let count = 0;
for (let i = 2; i < num; i++) {
    let flag = true;
    for (let j = 2; j <= Math.sqrt(i); j++) {
        if (i % j === 0) {
            flag = false;
            break;
        }
    }
    if (flag) {
        count++;
    }
}
console.timeEnd('质数');
console.log(`${num}以内质数总数:${count}`);
质数: 19.235107421875 ms
100000以内质数总数:9592

练习_99乘法表

// 打印99乘法表
for (let x = 1; x <= 9; x++) {
    for (let y = 1; y <= x; y++) {
        document.write(`${x} * ${y} = ${x * y} `);
    }
    document.write('<br>');
}

对象类型

对象可以存入多种数据类型的类型

创建对象

定义方式: {}new Object()Object()new Object

let a = {};
console.log(typeof a);
let  b = Object();
console.log(typeof b);
let c = new Object();
console.log(typeof c);

添加/修改属性

语法: 对象名.属性名 = 属性值;对象名['属性名'] = 属性值;

读取属性

语法: 对象名.属性名;对象名['属性名'];

删除属性

语法: delete 对象名.属性名;delete 对象名['属性名'];

注意

当读取对象中没有的属性时不会报错,而是返回undefined

let c = new Object();
console.log(c.name); // undefined

对象的属性名

1.对象的属性名通常情况下是一个字符串

2.如果是数字开头,或特殊字符,操作对象的属性格式: 对象名['属性名']

c['@'] = '123'
img

3.也可以使用变量名作为属性名,该变量的值可以是任何类型。操作对象的属性格式: 对象名[变量名]对象名[变量值]

let c = {};
let num = 1;
let str = '你好';
let arr = [1, 2];
let obj = { name: 1 };
let symbol = Symbol('123');
let nulls = null;
let undefineds = undefined;
let bool = true;
c[num] = 123; // 添加数字类型变量作为属性
c[str] = '你好'; // 添加字符串类型变量作为属性
c[arr] = [1, 2]; // 添加数组类型变量作为属性
c[obj] = { name: 1 }; // 添加对象类型变量作为属性
c[symbol] = Symbol('123'); // 添加symbol类型变量作为属性
c[nulls] = 'null1'; // 添加null类型变量作为属性
c[undefineds] = 'undefined1'; // 添加undefined类型变量作为属性
c[bool] = true; // 添加boolean类型变量作为属性
console.log(c); // 查看对象
console.log('string类型变量名读取', c[str]);
console.log('string类型变量值读取', c['你好']);
console.log('boolean类型变量名读取', c[bool]);
console.log('boolean类型变量值读取', c[true]);
console.log('number类型变量名读取', c[num]);
console.log('number类型变量值读取', c[1]);
console.log('object类型变量名读取', c[obj]);
console.log('object类型变量值读取', c[{ name: 1 }]);
console.log('null类型变量名读取', c[nulls]);
console.log('null类型变量值读取', c[null]);
console.log('undefined类型变量名读取', c[undefineds]);
console.log('undefined类型变量值读取', c[undefined]);
console.log('symbol类型变量名读取', c[symbol]);
console.log('不能通过symbol类型变量值读取', c[Symbol('123')]);

img
需要注意的是,用哪个变量作为属性名存数据,就需要用哪个变量名来读取数据,或者哪个变量对应的值读取

注意

symbol类型的变量作为属性名,只能通过变量名读取,不能通过变量值读取
当属性名是一个变量时需要用[]包裹


使用in运算符判断属于是否存在于对象中

语法: '属性名' in 对象名,返回true(存在)或false(不存在)

console.log('age' in obj); // 输出:false

对象字面量

object的字面量: {}{name: 'san'}[1,2]
string的字面量: '字符串1'"字符串2"
number的字面量: 1233.14
boolean的字面量: truefalse
null的字面量: null
undefined的字面量: undefined
bigint的字面量: 123n数字n
symbol没有字面量

使用对象字面量创建对象

语法:let 对象名 = { 属性名: 属性值 }
属性名如果是变量时: let 对象名 = { [变量名]: 属性值 }

let arr = [1,2];
let obj = {
    name: '张三',
    [arr]: '123',
    user: {
        name: '李四'
    }
};
console.log(obj);

img


对象的属性值简写方式

函数作为属性值

语法: 属性名: function (){}属性名(){}属性名: () => {}

let obj = {
    name: '张三',
    fn: function () {
        console.log(this);
    },
    // 简写
    fn2() {
        console.log(this);
    },
    fn3: () => {
        
    }
}
console.log(obj);

当属性值是一个变量,并且属性名和变量名相同时,可以省略属性名

 let fn = () => {
    console.log(this);
}
let age = 12;
let obj = {
    name: '张三',
    fn, // 简写为变量
    age // 简写
};
console.log(obj); // 输出: { name: '张三', fn: [Function: fn], age: 12 }

枚举属性

枚举属性: 是指对象中可以被某些方法遍历或操作的属性
通过for-in循环遍历对象,可以获取对象的所有属性名,但是不包括不可枚举属性

// 当symbol类型的变量作为属性时,该属性是不可枚举的  
let symbol2 = Symbol(1);
let obj = {
    anme: '张三',
    age: 20,
    friend: {
        name: '李四'
    },
    goodMan: true,
    [symbol2]: '213',
    [Symbol(1)]: '123'
}
for (let key in obj) {
    console.log(`属性名是${key} 属性值:`, obj[key]);
}

img

注意

当属性名是symbol类型的变量时,不能获取,也就不能枚举出来,也称为不可枚举属性


可变类型

javaScript执行上下文生成之后,会创建一个叫做变量对象的特殊对象,JavaScript 的基础数据类型往往都会保存在变量对象中。

严格意义上来说,变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其与堆内存区分开来。

// 变量对象
var a1 = 0;
// 变量对象
var a2 = 'Bingo!';
// 变量对象
var a3 = null;
// 变量 b 存在于变量对象中,{m: 20} 作为对象存在于堆内存中
var b = { m: 20 };
// 变量 c 存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中
var c = [1, 2, 3];

img

let a = 10;
let b = a;
b = 20;

在变量对象中数据发生赋值操作时,系统会自动为新的变量分配一个新值。let b = a 赋值操作执行后,虽然变量 a 和变量 b 均为 100,但是它们其实已经是相互独立互不影响的值了。

具体变化如下图所示:
img


let a = 1;
let b = a;
a = 2;
console.log(b); // 输出:1
let obj = { name: '张三' }; // obj变量对应的对象内存地址0x111
let obj2 = obj; // 只是把obj变量对应的对象内存地址0x111复制一份作为obj2的值  
obj.name = '李四';
console.log(obj2, obj2 == obj); // 输出:{ name: '李四' } true

上面代码中可以看出:
1.原始类型的变量对应的值一旦在内存中创建就不可修改
2.对象类型的变量对应的值在内存中创建后可以修改,修改后,引用了该地址变量的值也就修改了
3.当两个对象进行全等比较时,比较的是内存地址而后再比较值是否相等

注意

使用const关键字声明的常量,只会禁止常量重新赋值,并不会禁止常量对应的值进行修改

如下代码,对常量constObj赋值后,constObj对应的值是可以修改的,但不能重新对constObj赋值

let obj = { name: '张三' };
const constObj = obj;
obj.name = '王五';
console.log(constObj) // 输出:{ name: '王五' }

函数

函数定义方式

函数声明

语法:function 函数名(参数1, 参数2){}

function add(num1, num2) {
    return num1 + num2;
}

函数表达式

语法:const 变量名 = function(参数1, 参数2){}

const add = function(num1, num2) {
    
}

箭头函数

语法:const 变量名 = (参数1, 参数2) => {}

const add = (num1, num2) => {
    return num1 + num2;
}

当只有一行代码时,可以把花括号和return关键字删除,进行简写
语法:const 变量名 = (参数1, 参数2) => 表达式

const add = (num1, num2) => num1 + num2;

当只有一个参数时,可以省略括号
语法:const 变量名 = 参数 => 表达式

const calc4 = num => num + 1;
console.log(calc4(1)); // 输出:2

形参与实参

形参: 函数定义时的参数
实参: 函数调用时的参数

num1和num2是形参, 1和2是实参

function add(num1, num2) {
    return num1 + num2;
}
add(1,2);

函数调用过程,也就是实参赋值到函数形参的过程,同时也确定了函数形参的类型


函数不会检查参数类型是什么,也不会检查调用时传入的参数个数

如下代码,函数的形参num1,和num2可以接收任何类型的值
在调用calc3时只传入一个参数,也可以传入多个参数,没有语法错误

const calc3 = (num1, num2) => {
    console.log(num1); // 输出:1
    return num1 + num2;
}
console.log(calc3(1)); // 输出:NaN
console.log(calc3(1, 2, 3)); // 输出:3

函数形参默认值

语法: function 函数名(形参1 = 默认值1, 形参2 = 默认值2){}
定义形参默认值时,必须使用()包裹形参

const calc5 = (num=3) => num + 1;
console.log(calc5()); // 输出:4

注意

如果以字面量的形式定义默认值,那么每次调用函数,都会重新创建默认值
如果以变量名的形式定义默认值,并且该变量是对象类型,实际上是传递了对象的引用地址,函数中修改对象的值,函数外面的变量也会发生改变

字面量作为默认值:

const getName2 = (arg = { name: '张三' }) => {
    console.log(arg); // 打印arg对象
    arg.name = '李四';
}
getName2(); // getName2函数中打印出: { name: '张三' }
getName2(); // getName2函数中打印出:{ name: '张三' }

变量作为默认值:

let user = { name: '张三' };
const getName2 = (arg = user) => {
    console.log(arg); // 打印arg对象
    arg.name = '李四';
}
getName2(); // getName2函数中打印出: { name: '张三' }
getName2(); // getName2函数中打印出:{ name: '李四' }

对象作为函数参数

修改对象

let user = { name: '张三' };
const getName2 = (user) => {
    // user = {}; // 这里修改的是user变量  
    user.name = '李四'; // 这里修改的是user对象
}
getName2(user);
console.log(user); // 输出:{ name: '李四' }

字面量对象作为函数参数

let name = '张三'
const fn = ({ name1 }) => {
    console.log(name1);
}
fn({ name1: name }); // 输出: 张三

1.当调用 fn({ name1: name }) 时,JavaScript 会立即创建一个新的对象
2.函数 fn 接收到的参数,仍然是传入字面量对象地址引用
3.形参赋值: 函数中形参{name1}本质上在函数中声明了一个匿名对象,传入的字面量对象引用赋值给函数中的匿名对象

const fn = ({ name1 }) => { ... }
// 等价于:
const fn = (param) => {
  const { name1 } = param; // 解构 param 对象的 name1 属性
  console.log(name1);
}

修改变量

let user = { name: '张三' };
const getName2 = (user) => {
    user = {}; // 这里修改了user变量的引用地址,重新指向了新地址  
    // user.name = '李四'; // 这里修改的是user对象
}
getName2(user);
console.log(user); // 输出: { name: '张三' }

函数中修改对象: 在函数中没有对形参重新赋值时,形参仍然指向原来的对象,对该形参进行操作时,引用了该对象的变量的值也会发生改变
函数中重新赋值形参: 在函数中对形参重新赋值时,形参指向新的对象,对该形参进行操作时,引用了该对象的变量的值不会发生改变,只在函数内部生效,不会影响该变量的值


函数作为参数传递

const fn = () => {
    console.log('这是fn函数');
}
const print = (arg) => {
    arg(); // 调用传过来的函数
}
print(fn); // 输出: 这是fn函数
// 传入匿名函数
print(() => console.log('这是匿名函数')); // 输出: 这是匿名函数

函数的返回值

函数的返回值可以是一个函数

const fn = () => {
    return () => {
        console.log('返回的函数打印了');
    }
}
console.log(fn()()); // 调用函数并调用返回的函数 输出:返回的函数打印了

注意

1.当函数return不返回任何值时,或没有return时,该函数返回undefined

const fn1 = () => {return};
console.log(typeof fn1()); // 输出:undefined
const fn2 = () => {};
console.log(typeof fn2()); // 输出:undefined

2.return后面的代码不再执行,return后面仍有代码,没有语法错误,但return后面的代码会被忽略不会执行

const fn = () => {
    console.log('fn函数执行了');
    return;
    console.log('return后面的代码不会执行'); // 不会执行
}

2.定义函数形参实际上是在行数声明了一个let变量,但是没有赋值,函数内部可以使用该变量,当外部调用该函数时,函数内部的变量会被赋值

const fn = (arg) => {
    console.log(arg);
}
fn(); // 输出:undefined 而不是 arg is not defined
fn(1); // 输出:1

箭头函数返回值

当箭头函数只有一个表达式时,return可以省略

const fn = (arg1, arg2) => arg1 + arg2;
console.log(fn(1, 2)); // 输出:3

当箭头函数返回了字面量对象时,可以省略return,但是必须使用()圆括号包裹,用于区分函数体{}和对象{}

const fn = () => ({ name: '张三' });

方法

当一个函数作为一个对象属性值时,我们称这个函数就是该对象的方法,调用函数就称为调用该对象的方法

语法: 对象.方法名()

const obj = {
    user: () => {
        console.log('user');
    }
}
obj.user(); // 调用对象方法,就执行了user属性的函数,输出:user

函数的方法

call()和apply()

函数调用方式:
1.函数()
2.函数.call()
3.函数.apply()

call()和apply()指定this

call()和apply()都可以指定this是谁

function fn() {
    console.log('this:', this);
}
let obj = { name: '张三' };
fn(); // 输出: this: window
fn.call(obj) // 输出: this: {name: "张三"}
fn.apply(obj) // 输出: this: {name: "张三"}

call()和apply()传递参数区别

call(this, ...args): 可变参数
apply(this, [...args]): 固定参数

let obj2 = { name: '张三' };
function fn2(a, b) {
    console.log('a=', a, 'b=', b, this);
}
fn2.call(obj2, 1, 2); // 输出: a= 1 b= 2 {name: "张三"}
fn2.apply(obj2, [1, 2]); // 输出: a= 1 b= 2 {name: "张三"}

bind(): 绑定新函数this指向,永不可修改this指向,可以给新函数设置形参固定值

作用:
1.用于创建一个新函数
2.改变新对象的this指向,并且this指向不可改变
3.给新函数设置形参固定值

function fn() {
    console.log('this:', this);
}
let newFn = fn.bind();
console.log(newFn === fn); // 输出: false

通过bind使新函数绑定了this对象,而后再对新函数修改this对象,则this对象不会改变

function fn() {
    console.log('this:', this);
}
let obj = { name: '张三' };
let obj2 = { name: '李四' };
let newFn2 = fn.bind(obj)
newFn2(); // 输出: this: {name: "张三"}
newFn2.call(obj2); // 输出: this: {name: "张三"}

给新函数形参设置固定值

bind(this, ...args): 设置新函数形参固定值
设置一个固定值后,调用新函数时,固定值将传递给新函数的第一个形参,而传入的第一个参数将传递给新函数的第二个形参

function fn3(a, b) {
    console.log('a =', a, 'b =', b);
}
let obj3 = { name: '张三' };
let newFn3 = fn3.bind(obj3, 10);
// 由于设定了固定值,实际上函数传递为: newFn3(10, 20)
newFn3(20, 30); // 输出: a = 10 b = 20

function testBind(a, b){
  console.log('this', this);
  console.log('a', a, 'b', b);
}
let newFn = testBind.bind('this-', 1);
newFn(2);

img


箭头函数无法通过call()和apply()指定this

箭头函数的this无法通过call()apply(),bind()指定this,因为箭头函数中没有自己this,是由外层作用域中的this赋给箭头函数的this

let fn = () => {
    console.log('this:', this);
}
fn.call({}); // 输出: this: window
fn.apply({}); // 输出: this: window
fn.bind({})(); // 输出: this: window

函数的arguments隐藏属性

函数中的this和arguments都是隐藏对象

arguments类数组对象

class Person { }
console.log(new Person()); // 输出: Person {}
function fn() {
    console.log(arguments);
}
fn(1, 2); // 输出: Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]

上面输出我们可以看出,arguments是一个函数中的隐藏实例对象,不像普通类实例对象Person {}用花括号包裹成员,而arguments是类数组实例对象,其内容是一个数组

作用:
1.arguments用来存储用户传递的是实参,无论函数是否使用形参接收,arguments都会存储传过来的实参
2.可以通过数组语法直接读取实参,如arguments[0]arguments[1]arguments[2]

注意

console.log(Array.isArray(arguments)); // 输出: false
1.arguments可以通过索引读取
2.使用length属性获取长度
3.使用for-of遍历
4.但不能和数组一样调用数组方法,例如 arguments.forEach(()=>{}); // 报错: arguments.forEach is not a function

使用场景: 不依赖于函数形参,实现功能

function sum() {
    let sum = 0;
    for (let item of arguments) {
        sum += item;
    }
    return sum;
}
console.log(sum(1, 2)); // 求和

可变参数

在函数中直接使用arguments对象会存在以下问题
1.调用者不知道函数可以传参
2.不能使用数组方法

通过可变参数可以解决
语法: fn(...args)
含义: 可接收任意数量的参数,并存储在args数组中,如果...扩展扩展运算符在函数外使用,则是展开对象属性或元素

使用可变参数后,arguments仍可以使用

function sum2(...params) {
    console.log(params); // 输出: (2) [1, 'a']
    console.log(arguments); // 输出: Arguments(2) [1, 'a', callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
sum2(1, 'a');
function sum3(a, b, ...params) {
    console.log(a); // 输出: 1
    console.log(b); // 输出: 'a'
    console.log(params); // 输出: (2) [2, 3]
}
sum3(1, 'a', 2, 3);

箭头函数中没有arguments隐藏属性

let fn2 = () => {
    {
        console.log(arguments);
    }
}
fn2(); // 输出: arguments is not defined

作用域

作用域: 指的是变量/函数的可见性区域,变量的可见性决定了变量/函数的作用域,变量的作用域决定了变量的生命周期

全局作用域

全局作用域: 在整个页面都可以访问的作用域
创建: 页面加载时创建
销毁: 页面关闭时销毁

所有直接编写到script标签中的变量都位于全局作用域中

<script>
    let a = 1; // 全局作用域
    const fn = () => 1; // 全局作用域
    {
        let b = 2; // 局部作用域
    }
</script>

局部作用域

块作用域: 变量只在代码块中可见,代码块以外无法使用
创建: 代码块执行时创建
销毁: 代码块执行完毕后销毁
局部作用域可以嵌套

{
    let a = 1; // 变量a位于局部作用域
}
{
    {
        let b = 2; // 变量b只在最近的代码块中使用,所以位于局部作用域,作用域是最近的代码块
    }
    console.log(b); // b is not undefined
}

函数作用域

函数作用域: 只在函数中可见的区域,即函数体就是函数的作用域,函数执行完毕后销毁
生命周期:函数作用域的生命周期从函数被调用时开始,到函数执行完毕时结束。在这个过程中,作用域内的所有局部变量都是有效的。
每次调用函数都会产生全新的函数作用域,相同函数作用域之间互不影响

变量b的作用域就是函数作用域

function fn2() {
    let b = 1;
}
console.log(b); // 报错: b is not defined

声明式函数本身所属作用域是全局作用域

声明式函数的作用域属为全局作用域,成为window的方法

{
    const fn0 = function () {
        console.log('fn0');
    }
    const fn = () => console.log('fn');
    function fn2() { console.log('fn2'); }
}
console.log(fn2()); // 输出: fn2
console.log(fn0()); // 报错: fn0 is not defined
console.log(fn()); // 报错: fn is not defined

函数优先寻找相同作用域的资源,如fn和代码块中的a属于相同作用域,因此无论在什么位置调用fn时,fn中的变量a访问的是相同作用域的a=2

let a = 1;
{
    let a = 2;
    function fn() {
        console.log(a);
    }
}
console.log(fn()); // 输出: 2

嵌套的声明式函数所属作用域是函数作用域,不属于全局作用域

let a = 1;
function fn() {
    // 函数作用域
    function fn2() {
        console.log(a);
    }
    fn2();
}
fn(); // 输出: 1
console.log(fn2()); // 报错: fn2 is not defined

类不属于全局作用域

class Person1 { }
{
    class Person { }
}
console.log(new Person1()); // 输出: Person1 {}
console.log(new Person()); // 报错: Person is not defined

总结

js引擎加载代码: 加载声明<script>标签中的变量、函数、代码块中(变量、函数等)、类。不会加载声明对象内部代码,如函数内部的变量,类内部的变量等,只有在调用时才会加载声明,并且确定成员所属的作用域,如函数中的成员所属作用域是函数作用域,且函调执行完毕后,函数作用域销毁。

1.函数作用域

是针对于函数成员来定义的,也是说成员所属的作用域仅限于所属函数体内,因此称为函数作用域,函数作用域在调用和执行完毕创建和销毁

2.块级中变量所属作用域

js引擎加载代码时产生

3.类所属于作用域

js引擎加载代码时产生

4.函数所属作用域

不是嵌套函数:
js引擎加载代码时产生,如声明式函数加载后它的所属作用域为全局作用域(如果属于嵌套函数,仍受制于外层函数作用域),而表达式函数或箭头函数不属于全局作用域
是嵌套函数:
外部函数调用时,嵌套函数所属作用域创建,调用完毕嵌套函数所属作用域销毁,即使嵌套的是声明式函数,它所属的函数作用域也会销毁,受制于函数作用域,不会提升为全局作用域

5.变量所属作用域

var声明的变量: js引擎加载代码时产生,属于全局作用域,受限于函数作用域,不会提升为全局作用域
let声明的变量: 受限于代码块作用域,函数作用域

6.词法作用域

词法作用域: 词法作用域(Lexical Scope) 是指在编程语言中,作用域的确定是基于代码的物理结构,而不是运行时的动态决定。具体来说,词法作用域是由变量和块的定义位置决定的,编译器在解析代码时会保持作用域不变

7.嵌套函数作用域在调用外层函数时其所属作用域就已经确定了(词法作用域),嵌套函数不会因为不同位置被调用而改变他们的作用域


作用域链

作用域链: JS解释器优先在最近的作用域中寻找资源,然后再去父作用域中寻找,直到全局作用域,对于该资源的可见性区域就形成了一个作用域链

在代码块中访问fn()资源,js解释器网上层层寻找
img


window对象

浏览器提供的一个window对象,通过window对象可以对浏览器窗口进行操作,除此之外window对象还负责存储js的内置对象浏览器宿主对象
JS内置对象: MathDateJSONStringNumber
浏览器宿主对象: historylocationnavigatorconsolealert
JS内置对象和浏览器宿主对象都可以直接使用

调用window对象的方法: window.alert('1')简写: alert('1')window.可以省略,直接使用浏览器的宿主对象方法

let obj = { name: '张三' };
let objJson = JSON.stringify(obj); // JS内置对象
window.console.log(objJson); // window对象调用console方法  

注意

向window对象添加自定义属性时,该属性称为全局属性,因为window属于全局对象

 {
    {
        {
            window.customerAttr = '123'; // window对象添加属性
        }
    }
    {
        console.log(customerAttr); // 输出:123
    }
}

函数中向window添加属性,需要调用函数后,执行window.bbb1='李四'后,该属性才是全局属性

{
    function bbb() {
        window.bbb1 = '李四';
    }
}
bbb();
console.log(bbb1); // 未调用bbb()时输出undefined,调用bbb()后输出:李四

不在函数中使用var声明变量时,等价于向window添加属性,因此var声明变量是全局变量

{
    var name = '张三';
}
console.log(window.name); // 输出:张三

在函数中使用var声明的变量,仍然有函数作用域,不是全局变量,也就是说var不具有块级作用域但有函数作用域

function aaa() {
    var aaa1 = '张三';
}
aaa();
console.log(window.aaa1); // 输出:undefined

使用function关键字声明的函数会作为window的属性值,实质上是window.函数名

{
    function fn() {
        console.log('这是函数声明定义的函数都是全局函数');
    } 
}
console.log(fn); // 输出:[Function: fn]
window.fn(); // 输出:这是fn函数

而表达式或者箭头定义的函数不是全局函数

{
  // 表达式函数不具有全局作用域,仍被代码块限制  
  let fn = function () {
    console.log("这是表达式函数");
  };
  // 声明式函数具有全局作用域  
  function fn2() {
    console.log("这是函数声明");
  }
}
// 箭头函数不具有全局作用域   
let fn3 = () => console.log("这是箭头函数");
fn2(); // 这是函数声明
fn(); // fn is not defined
fn3(); // 这是箭头函数
window.fn3(); // window.fn3 is not a function

使用let关键字声明的变量不会存在window中,而是存储在script块级作用域中

let obj = { name: '张三' };
{
    function aaaa() {
        console.log('这是函数声明定义的函数都是全局函数');
    }
}
let fn2 = () => console.log('使用箭头定义函数不是全局函数');

通过devtools查看script块级作用域中有objfn2,global作用域中只有声明式函数aaa
img


let awindow.a作用域不同,因此window.alet a是两个不同的变量

let a = 1;
window.a = 2;
console.log(a);  // 输出:1
console.log(window.a); // 输出:2

变量与函数提升

var关键字的提升

变量提升: 它指的是变量和函数声明在代码执行之前就被提升到当前作用域的顶部

正常情况下,在使用了未声明的变量时,会报错,is not defined

console.log(a);
a = 1; // a is not defined

如果先使用,再使用var声明,则不会报错,会打印出undefined

console.log(a);
var a = 1; // 输出:undefined

也就是说var声明的变量,像是在使用前已经声明了,和声明变量的位置无关,这就是变量提升,但不会赋值,因此是undefined


声明式函数提升与赋值

正常情况下,表达式或者箭头式函数在使用前,会报错,fn is not a function

fn(); // 输出: fn is not a function
var fn = function () {
    console.log('表达式函数');
}

如果是使用声明式函数,就会使函数提升,不会报错,可以正常使用函数

fn2(); // 输出: 声明式函数
function fn2() {
    console.log('声明式函数');
}

console.log(fn); // undefined
console.log(fn2); // fn2 is not defined
console.log(fn()); // fn is not a function
{
  function fn() {
    console.log(" 这是函数声明 ");
  }
  let fn2 = function () {
    console.log(" 这是函数表达式 ");
  };
}

在编译阶段,声明式函数会声明提升,但赋值仍然受到作用域限制,表达式函数或箭头函数不会声明提升,更不会赋值


声明式函数会作为window的全局属性,但赋值会受代码块的影响,只有在代码块加载完成后才会赋值,而不在代码块的声明函数,会声明并赋值

console.log(a); // 输出: undefined 说明a是全局变量,但由于代码块的影响并未给a赋值
{
    {
        function a() {
            console.log('a');
        }
    }
}
console.log(a); // 输出: f a() 说明代码块加载后,也就给a赋值了,a的值是一个函数
console.log(fn); // 输出: undefined
console.log(fn2); // 输出: f fn2()
{
    function fn() { console.log('fn') };
}
function fn2() { console.log('fn') };

为什么要提升

早期浏览器处理代码的能力非常有限。为了提高执行速度,引擎在代码执行前会进行简短的预编译阶段(Parse Phase):
收集声明:预编译阶段会快速扫描代码,将varfunction声明提前记录在内存中,形成作用域的变量对象
执行阶段优化:跳过声明步骤,直接处理赋值和逻辑,这使得执行阶段更高效


总结

1.var声明的变量会提升,但赋值留在原地

console.log(a); // 输出: undefined
var a = 1;
console.log(a); // 输出: 1

2.var没有块级作用域但有函数作用域

{
    var a = 1;
}
function aaa() {
    var b = '张三';
}
console.log(a); // 输出: 1
console.log(b); // 输出: b is not a defined

3.let关键字声明的变量也会提升,但是解释器禁止在赋值前访问

console.log(a); // 输出: a is not a defined
a = 1;

console.log(a); // 输出: ReferenceError: Cannot access 'a' before initialization
let a = 1;

从上面的代码可以看出,先使用let声明的变量a没有提示is not a defined错误,而是提示了Cannot access 'a' before initialization错误,
说明let声明的变量也会提升,但是解释器禁止在赋值前访问


4.未使用关键字声明的变量,在js加载后变为全局变量(对变量的作用域造成混乱,需要禁止类似书写)

function aaa() {
    aaa1 = 1;
}
aaa(); // 执行函数后,函数内部无使用关键字声明的变量会变成全局变量
console.log(window.aaa1); // 未调用aaa()时输出undefined,调用aaa()后 输出:1
console.log(aaa); // 输出: aaa is not defined
{
    {
        aaa = 1;
    }
    {
        console.log(window.aaa); // 输出:1
    }
}
console.log(aaa); // 输出:1

5.声明式函数整体提升:function fn() {} 的声明和定义会完整提升到作用域顶部,window.fn = function(){},而表达式或者箭头函数不会整体提升,可通过var关键字使声明提升

console.log(window.fn); // 输出: f fn()
console.log(fn2); // 输出: undefined
function fn() {
    console.log('这是声明式函数');
}
var fn2 = function () {
    console.log('这是表达式函数');
}

6.同名标识符的覆盖规则:当声明式函数与var变量声明同名时,声明式函数的整体提升优于var变量,但后续的var变量赋值会覆盖优先声明并定义的函数

两个例子说明

例子1:

console.log(fn); // 输出: f fn() 声明式函数整体提升优于var,因此输出函数 fn()
var fn = 1; // 又对fn变量赋值 ,此时fn = 1
function fn() { console.log('fn') };
console.log(fn); // 输出: 1 

例子2:

console.log(fn2); // 输出: undefined
var fn2 = 1;
{
    function fn2() { console.log('fn') };
}
console.log(fn2); // 输出: f fn()

7.声明式函数的定义(也就是赋值)受代码块影响,代码块加载完成后会对变量赋值为函数,赋值时不受代码块嵌套层数限制,只受代码块位置限制

console.log(fn); // 输出: undefined
{
    function fn() {
        console.log('这是声明式函数');
    }
}
console.log(fn); // 输出: f fn() 

8.声明式函数会覆盖掉之前的函数定义

console.log(a); // 打印出第二个函数 f a() {alert(2)}
function a() {
    alert(3);
}
function a() { // 会把前面的定义覆盖
    alert(2);
}

练习_变量提升与作用域

在函数内部也会存在变量提升var a = 2;提升为var a;那么在log时,就会打印出undefined

var a = 1;
function fn() {
    console.log(a); // 打印出undefined
    var a = 2;
}
fn();

在fn中找不到a,就会根据作用域链向上查找直到找了全局变量a,又重新对a进行赋值,因此fn()下面的console.log(a)会打印出2

var a = 1;
function fn() {
    a = 2;
    console.log(a); // 打印出1
}
fn();
console.log(a); // 打印出2

第一次执行console.log(a)时,在fn内部找不到a变量,就会根据作用域链向上查找直到找了全局变量a,因此会打印出1
又重新对a进行赋值,第二次打印出2,因为已经对全局变量a赋值了,第三次打印出2

var a = 1;
function fn() {
    console.log(a); //第一次 打印出1
    a = 2;
    console.log(a); //第二次 打印出2
}
fn();
console.log(a); //第三次 打印出2

var a = 1;
function fn(a) {
    console.log(a); // 打印出1
    a = 2;
    console.log(a); // 打印出2
}
fn(a);
console.log(a); // 打印出1

console.log(a); // fn a(){alert(2)}
var a = 1;
console.log(a); // 1
function a() {
    alert(2);
}
console.log(a); // 1
var a = 3;
console.log(a); // 3
var a = function () {
    alert(4);
}
console.log(a); // fn a() {alert(4)}
var a; // 重新声明,不会修改已经存在的变量值  
console.log(a); // fn a() {alert(4)}

控制台调试

调试状态下监控某一个变量实时变化

双击变量添加到watch
img


立即执行函数(IIFE)

IIFE: Immediately Invoked Function Expression

语法: (function () {})();
作用: 自动执行函数,避免全局变量污染

可以写多个立IIFE,

(function () {
    var a = 1;
    console.log(a); // 输出: 1
})();
(function () {
    var a = 2;
    console.log(a); // 输出: 2
})()

this

this是一个动态对象,要么是调用方法的对象,要么是window对象

1.以函数方式调用,函数中的this就是window对象

let fn = function () {
    console.log(this);
}
fn(); // 输出: window对象

2.以方法方式调用,对于声明式或表达式函数中的this就是调用者对象

如下代码,声明式或者表达式函数被对象调用了,那么函数中的this就是该对象本身

let fn = function () {
    console.log(this == obj);
}
function fn2() {
    console.log(this == obj);
}
let obj = { fn: fn, fn2: fn2 };
obj.fn(); // 输出: true
obj.fn2(); // 输出: true

3.对于箭头式函数没有自己的this,和调用方式无关,其中的this与最近非箭头函数或全局作用域的this保持一致

箭头函数没有自己的this,它会捕获其所在上下文的this值,即父级作用域的this。换句话说,箭头函数的this与其外层最近的非箭头函数的this保持一致

如下
fn是非箭头函数,它的this受调用方式的影响,window.obj2.fn()是调用了obj2的方法方式调用函数,所以fn中的this是调用者对象自己(obj2)
fn2是箭头函数,obj2是通过对象字面量定义的,而对象字面量不会创建新的作用域,因此obj2不是fn2最近的作用域,因此fn2的箭头函数实际上定义在全局作用域中(即 obj2 所在的上下文)就是<script>标签的作用域,<script>中this就是window
fn33是箭头函数,它的父级作用域fn3,而fn3也是箭头函数,因此fn3的this和fn2的this一致,都是window
fn44是箭头函数,它的父级作用域fn4,而fn4不是箭头函数,因此fn44的this和fn4的this一致,而fn4的this是调用者对象,为obj2

console.log(this); // 输出:window
var obj2 = {
    name: '张三',
    fn() {
        console.log(this); // obj2实例
    },
    fn2: () => {
        console.log(this); // window
    },
    fn3: () => {
        console.log(this); // window
        const fn33 = () => {
            console.log(this); // window
        }
        fn33();
    },
    fn4() {
        console.log(this); // obj2实例
        const fn44 = () => {
            console.log(this); // obj2实例
        }
        fn44();
    }
}
window.obj2.fn();
obj2.fn2();
obj2.fn3();
obj2.fn4();

4.在构造函数中this就是新建的对象

class A {
    constructor(){
        // this也是b实例
    }
}
class B extends A {
    constructor() {
        // this就是b实例
        super(); // super会把子类中this传递给父类构造
    }
}
let b = new B();

5.通过call()和apply()调用函数时,它们的第一个参数指定this对象,修改函数中this

6.类中的箭头函数或捕获类的构造方法中的this,也就是说类中箭头函数的this就是类实例

class Test {
    fn2 = (obj) => {
        console.log('this === obj', this === obj);
    }
}
let test = new Test();
test.fn2(test); // 输出: this === obj true

总结this

1.对于声明式或者表达式函数中的this,谁调用函数,this就是谁,this可以是window对象也可以是调用者对象
2.对于箭头函数中没有自己的this,其中的this与最近非箭头函数或全局作用域的this保持一致
3.构造函数中始终是new出来的新实例对象,父类构造中也是new出来的新子类实例对象
4.通过call()和apply()调用函数时,它们的第一个参数指定this对象,修改函数中this
5.箭头函数中的this值不能通过call()和apply()修改
6.通过bind()方法对新函数绑定this指向,并且新函数中的this指向永不可改变

this的作用

声明式或者表达式函数中的this指向调用函数的对象,通过this可以对对象进行操作


严格模式

JS运行运行代码的模式有两种

1.正常模式(默认模式)

语法检查并不严格
原则: 能报错的地方尽量不报错

2.严格模式

语法检查严格
1.禁止一些语法
2.更容易报错
3.性能提升

开启方法: 在<script>标签中第一行添加use strict,如果use strict前仍有代码则失效

例如,开启严格模式未声明的变量不能使用,在使用时会报未声明

<script>
    "use strict";
    a = 1;
    console.log(a); // 输出:ReferenceError: a is not defined
</script>

全局严格模式和函数严格模式

全局严格模式:

<script>
    "use strict";
    function fn() {
        c = 0;
        console.log(c); // c is not defined
    }
    fn();
</script>

函数严格模式: 对函数外面的代码不进行严格检查,同样也需要写在函数的第一行,否则失效

<script>
    b = 0;
    console.log(b); // 输出:0
    function fn() {
        "use strict";
        c = 0;
        console.log(c); // c is not defined
    }
    fn();
</script>
posted @ 2025-04-18 15:43  ethanx3  阅读(35)  评论(0)    收藏  举报