JavaScript 是一门动态的弱类型的,解释型的,基于对象脚本语言。

动态: 程序执行的时候才会确定数据类型。 静态: 书写代码的时候提前确定数据类型。

弱类型: 数据类型可以自动转换。 强类型: 数据类型无法自动转换。

解释型: 边解释,边运行,开发效率更高。 编译型: 编译后运行二进制文件。

脚本: 一般都是可以嵌在其它计算机语言当中执行。

◆javaScript 的运行环境(解释器):

  1. 浏览器,如Chrome浏览器中的V8引擎。
  2. Node.js。

◆浏览器端的 JavaScript 组成部分:

  1. 基本代码语法, ECMAScript,ECMA指定。
  2. BOM, 浏览器提供的API,W3C指定。
  3. DOM, 文档提供的API,W3C指定。

◆其他特点:

  1. 指令结束符(语句结束符)是分号或者换行。
  2. 严格区分大小写。

开始

JavaScript 在 HTML 中使用的三种方式:
① 行内式(内联脚本)

1
<元素 onclick="代码.." ondblclick="代码.."></元素>

② 内嵌式(嵌入脚本)

1
2
3
<script>
代码
</script>

③ 外链式(外部脚本)

1
<script src="js文件的地址"></script>	

JavaScript如何输出内容:
① 输出到弹框:alert(内容)。
② 输出到页面中:document.write(内容)。
③ 输出到控制台:console.log(内容)。

变量

JS变量名的命名规范:

  1. 变量名可以由数字、字母、下划线、$ 组成且不能以数字开头
  2. 变量名不能是关键字或保留字。

var

var 是 JS 早期的变量声明关键字,存在诸多设计缺陷,现在几乎被 let/const 完全替代。

◆核心特点:
1.变量提升:变量声明会被提升到当前作用域顶部,提升后自动初始化为 undefined,可以在声明前使用变量。

全局代码执行之前会预处理, 查找全局代码中的var关键字,提前创建好变量,不赋值; 当正式执行到变量声明语句的时候,仅仅进行赋值操作。

1
2
3
4
5
6
7
8
9
10
console.log(n); //var提升,n为undefined

if (n === undefined) {
  n = 200;
  console.log(n); // 200
}

console.log(n); // 200
var n = 100; // 后声明变量
console.log(n);  // 100

2.允许重复声明:同一作用域内,可以多次用 var 声明同一个变量,不会报错,后续声明会覆盖前面的。
3.无块级作用域仅支持全局作用域和函数作用域iffor{} 等块级代码块无法形成独立作用域,变量会泄露到块外。

1
2
3
4
5
6
// for循环经典坑:变量泄露,回调访问全局变量
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i); // 输出:5 5 5 5 5(所有回调访问同一个全局 i)
  }, 100);
}

这里还涉及到同步事件与异步事件,后面再学。

let

let 是 ES6 为解决 var 的缺陷而新增的关键字,专门用于声明「值会动态变化」的变量。

◆特点:变量值可修改。不存在变量提升。在变量声明之前的区域,变量不可访问。let声明的范围是块作用域。允许先声明,后赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
// let:可变变量,块级作用域(只在{}内生效)
let age = 18;
age = 20; // 可以修改

if (true) {
let age = 25; // 局部变量,不影响外部
console.log(age); // 25
}

console.log(age); // 20

let b; //先声明
b = 10; //后赋值

let也不允许同一个块作用域中出现冗余声明。不过,JavaScript引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明

1
2
3
4
5
6
7
8
9
10
11
var name; 
var name;
let age;
let age; // SyntaxError;标识符age已经声明过了
---------------------------------------------
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}

◆与var关键字不同,使用let在全局作用域中声明的 变量不会成为window对象的属性(var声明的变量则会)​。

◆条件声明:因为let的作用域是块,所以不可能检查前面是否已经使用let声明过同名变量。

不要使用“如果某个条件成立,就let 某某”形式来声明变量,一个是因为let没法检查上方是否声明过“某某”这个变量,会报错(实际上在if语句之外也要注意这一点,这是个一般问题),另一个是因为“if(){}”“try{}/catch(){}”“typeof”等结束后,这个“某某”就被销毁了,对下方代码来说,“某某”跟未定义没有区别。

◆for循环中的let声明:(包括for-in/for-of循环)
在使用let声明迭代变量时,JavaScript引擎在后台会为每个迭代循环声明一个新的迭代变量。每个setTimeout引用的都是不同的变量实例,所以console.log输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。

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

const

常量,不允许重复声明。声明时必须同时初始化

1
2
3
4
5
6
7
8
9
10
// const:常量,声明后不可修改(引用类型内部可改)
const PI = 3.1415;
// PI = 3; // 报错!常量不能重新赋值

const user = { name: "小明" };
user.name = "小红"; // 允许!const只锁定引用,不锁定对象内部
console.log(user.name); // 小红

// 避坑:const声明还必须初始化,let可以先声明后赋值
// const a; // 报错

const只锁定引用,不锁定对象内部”这一点类似Java:final关键字对于对象引用,这意味着引用本身不可变(即不能指向另一个对象),但对象的内容仍然可以修改。

◆关于循环迭代:不能用const来声明迭代变量,因为迭代变量会自增。不过,如果只想用const声明一个不会被修改的for循环变量,那也是可以的。也就是说,每次迭代只是创建一个新变量。 比如for-of和for-in循环。

1
2
3
4
5
6
7
8
9
    for (const key in {a: 1, b: 2}) {
      console.log(key);
    }
    // a, b

    for (const value of [1,2,3,4,5]) {
      console.log(value);
    }
    // 1, 2, 3, 4, 5

💡声明风格及最佳实践:

  1. 不使用var。
  2. const优先,let次之。优先使用const来声明变量,只在提前知道未来会有修改时,再使用let。

数据类型

类型 包含的具体类型 核心特点
基本数据类型(值类型) Number、String、Boolean、Null、Undefined、Symbol(ES6 新增)、BigInt(ES6 新增) 1. 存储在栈内存中,直接存储值本身。
2. 赋值 / 传参时,传递的是值的副本,修改副本不会影响原数据。
3. 类型简单,大小固定。
引用数据类型(复杂数据类型) Object(包含子类型:Array、Function、Date、RegExp 等) 1. 值存储在堆内存中,栈内存只存储堆内存的地址引用。
2. 赋值 / 传参时,传递的是地址引用的副本,修改新变量会影响原数据。
3. 类型复杂,大小不固定,可动态扩展属性 / 方法。

判断数据的类型:typeof(数据)。不加括号可也行,因为typeof是一个操作符而不是函数。

  • 调用typeof null返回的是”object”。这是因为特殊值null被认为是一个对空对象的引用。

Null 和 undefined

◆null:Null类型同样只有一个值,即特殊值null。null值表示一个空对象指针,这也是给typeof传一个null会返回”object”的原因。

1
2
    let car = null;
    console.log(typeof car);   // object

◆undefiend :未定义,没有赋值的变量在使用的时候会自动得到undefined。增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。

使用typeof操作符时,如果返回undefiend,仅代表该变量当前是否存在,而不能说明该变量一定是undefiend类型。

1
2
3
4
5
    let message;     // 这个变量被声明了,只是值为undefined
    // 确保没有声明过这个变量
    // let age
    console.log(typeof message); // undefined
    console.log(typeof age);      // undefined

二者联系:undefined值是由null值派生而来的,因此ECMA-262将它们定义为表面上相等。用等于操作符比较null和undefined始终返回true。但要注意,这个操作符会为了比较而转换它的操作数。

1
console.log(null == undefined); // true

Boolean 布尔类型

有两个字面值:
true 表示是、肯定、正确
false 表示否、否定、错误

💡注意,布尔值字面量true和false是区分大小写的,因此True和False是有效的标识符,但不是布尔值。

◆要将一个其他类型的值转换为布尔值,可以调用特定的Boolean()转型函数。Boolean()转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值。转换规则如下:

数据类型 转化为true的值 转化为false的值
Boolean true false
String 非空字符串 “ “(空字符串)
Number 非零数值 0,NaN
Object 任意对象 null
Undefined N/A(不存在) undefined

if等流控制语句会自动执行其他类型值到布尔值的转换。

Number 数值类型

表示整数和浮点数(小数),JS 不区分整数和浮点型,统一用 Number 表示。

浮点数的运算存在精度问题:

1
0.1 + 0.2;    // 0.30000000000000004

我们日常使用的是十进制(满 10 进 1),而计算机底层只能识别二进制(满 2 进 1)。

  • 有些十进制整数或小数,能精确转换为二进制:比如 10(十进制)= 1010(二进制),0.5(十进制)= 0.1(二进制);
  • 但有些十进制小数,无法被二进制精确表示,会变成无限循环的二进制小数比。如 0.10.2,就像十进制无法精确表示 1/3 = 0.333333... 一样。

科学计数法

1
2
3
1.3e4; 	 // 13000 (1.3 * 10^4)
1.67e78; // 1.67 * 10^78
2.3e-2 // 0.023; (2.3 * 10^(-2))

③ NaN
1.什么是 NaN
NaN,全称 Not a Number,是 number 类型的一种。比如,用0除以0就会得到NaN。、

如果分子是非0值,分母是有符号0或无符号0,则会返回Infinity或-Infinity。

2.NaN 的特点
NaN 与任何数字进行任何计算结果都是 NaN。
NaN 与任何数字都不相等,包括自己。

3.isNaN() 函数
把一个值传给isNaN()后,该函数会尝试把它转换为数值。任何不能转换为数值的值都会导致这个函数返回true。

④JavaScript 中数字的有效范围

  1. JS 中能表示的最大的数字Number.MAX_VALUE:1.7976931348623157e+308。
  2. JS 中能表示的最小的正数:5e-324。
  3. 如果超出有效范围,用 Infinity、-Infinity 表示
  4. 函数isFinite()可以判断一个数字是否是有效数字,如果是有效数字结果是true。
    无效数字: Infinity、-Infinity、NaN。

数值转换:Number()、parseInt()、parseFloat()
Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
◆Number()函数基于如下规则执行转换。

  • 布尔值,true转换为1,false转换为0。
  • 数值,直接返回。
  • null,返回0。
  • undefined,返回NaN。

parseInt()函数更专注于字符串是否包含数值模式。第二个参数用于指定进制数。

  • 字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回NaN。

    这意味着空字符串也会返回NaN(这一点跟Number()不一样,它返回0)​。

  • 如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。
1
2
let a = parseInt('123abc'10);//按十进制转换
console.log(a); // 123

parseFloat(),第一次出现的小数点是有效的,但第二次出现的小数点就无效了。比如”22.34.5”将转换成22.34。parseFloat()只解析十进制值,因此不能指定底数。

String 字符串类型

字符串是不可变的。
表示:

1
2
3
4
5
let str1 = 'Hello World'; // 单引号 
let str2 = "JavaScript 数据类型"; // 双引号
let name = "张三";
let str3 = `你好,我是${name}`; // 反引号(模板字符串,${} 嵌入变量)
let str4 = ''; // 空字符串

➢为什么字符串不可变还能使用let?

1
2
3
4
5
6
7
8
9
let str = "Hello"; 

str[0] = "M"; // 尝试修改字符串的第0个字符(想把 H 改成 M)

console.log(str); // 输出结果:Hello(原字符串完全没变化,说明字符串本身不可变)

// 看似「修改」字符串的操作,其实是创建了新字符串
str = str + " World";
console.log(str); // 输出:Hello World

转义字符:

1
2
3
4
5
\n			换行
\' 单引号
\" 双引号
\\ 转义\本身
\uXXXX 四位十六进制表示unicode字符串

字符串的长度可以通过其length属性获取。
转换为字符串:使用几乎所有值都有的toString()方法。null和undefined值没有toString()方法。

1
2
let a = 10;
console.log(a.toString(8)); // "12"

与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串。
字符串插值通过在${}中使用一个JavaScript表达式实现。所有插入的值都会使用toString()强制转型为字符串,而且任何JavaScript表达式都可以用于插值。

Object 对象

广义:一切皆对象,数组、函数都是对象的一种。
狭义:Object 数据类型,是对象类型中的一种,与Array、Function 是平级的。

  1. Object类型的数据是值的无序集合。
  2. Object类型的数据由属性组成,属性由属性名和属性值组成。
  3. 属性值可以是任何类型的数据; 属性名用字符串表示,如果符合标识号规范,可以省略引号。
  4. 如果属性的值是一个函数,该属性可以被称为方法

创建对象

1.直接量方式(最常用)

1
2
3
4
5
6
7
8
9
10
11
const obj1 = {
  username: 'March',
  age: 20,
  getinfo: function(){
    console.log(`用户名: ${this.username}, 年龄: ${this.age}`);
  }
};

// 验证对象创建成功
console.log(obj1);
obj1.getinfo(); // 调用对象方法

2.Object()是一个内置函数,传入参数时会将参数转换为对应类型的对象,无参数时返回一个空对象。

1
2
3
4
5
6
7
8
// 无参数:返回空对象 
const obj2_1 = Object();
console.log("无参数创建空对象:", obj2_1); // {}

// 有参数:将参数转换为对象
const obj2_2 = Object("我是字符串");
console.log("传入字符串创建对象:", obj2_2); // [String: '我是字符串']
console.log("obj2_2 的长度:", obj2_2.length); // 5

3.使用new Object()构造函数

1
2
3
4
5
6
7
8
let obj1 = new Object();
obj1.name = "March";
obj1.age = 18;
obj1.getInfo = function(){
    console.log(`用户名:${this.name}  年龄:${this.age}`);
};

obj1.getInfo();  // 用户名:March 年龄:18

对象属性的读写

1.点号,简洁易用。
2.方括号,适合属性名不符合标识符规范、或使用变量表示属性名的场景。

1
2
3
4
5
6
7
8
9
10
let obj1 = new Object(); //创建对象
obj1.name = "March"; //添加属性和值,使用点号
obj1.age = 18;
obj1['home-address'] = "四川达州"; //添加属性和值,使用方括号
obj1.getInfo = function(){
    console.log(`用户名:${this.name}  年龄:${this.age}`);
};

console.log(obj1.name);
console.log(obj1['home-address']);
  1. 读取不存在的属性,自动得到 undefined。
  2. 给不存在的属性赋值,自动添加该属性。

对象属性的遍历

1
2
3
4
5
6
7
8
let obj1 = {
    name: "March",
    age: 18,
    ['homa-address']: "四川达州",
    getInfo: function(){
        console.log(`name: ${this.name}, age: ${this.age}, address: ${this['homa-address']}`);
    }
}

◆方式 1:for...in 循环(传统方式,遍历可枚举属性,包含继承属性),最常用的对象遍历方式,会遍历对象自身的可枚举属性,以及从原型链继承的可枚举属性。

1
2
3
4
//遍历对象的属性prop
for( let prop in obj1){
    console.log(prop + ": " + obj1[prop]);
}

hasOwnProperty():判断属性是否是对象自身的属性(过滤继承属性)

◆方式 2:Object.keys(对象名)(返回对象自身的可枚举属性名数组,不包含继承属性),返回一个数组,包含对象自身的所有可枚举属性名(字符串类型),不包含继承属性和不可枚举属性

1
2
3
4
5
6
// Object.keys():返回属性名数组
let props = Object.keys(obj1);
console.log(props); // [ 'name', 'age', 'homa-address', 'getInfo' ]
props.forEach( prop =>{
    console.log(prop + ": " + obj1[prop]);
} )

◆方式 3:Object.entries()(返回对象自身的可枚举属性键值对数组,不包含继承属性)
返回一个二维数组,每个子数组是 [属性名, 属性值],方便同时获取属性名和属性值。

1
2
3
4
5
6
[
[ 'name', 'March' ],
[ 'age', 18 ],
[ 'homa-address', '四川达州' ],
[ 'getInfo', [Function: getInfo] ]
]
1
2
3
4
5
6
7
8
9
10
// Object.entries():返回键值对数组
let obj1arr = Object.entries(obj1);
console.log(obj1arr);

// obj1arr.forEach( item => {
//     console.log(item[0] + ": " + item[1]);
// })
obj1arr.forEach( ([prop, value]) => {
    console.log(prop + ":" + value);
})

删除对象中的属性

使用 delete 运算符可以删除对象的自身属性(无法删除继承属性、不可配置属性),删除成功返回 true,删除失败返回 false

1
2
3
4
let deleteprop = delete obj1.getInfo;
console.log(deleteprop); // true
console.log(obj1);
// obj1.getInfo(); // 报错!不存在该方法。

判断对象中是否存在某个属性

◆方式 1'属性名' in 对象,返回布尔值,存在返回 true,不存在返回 false,会包含从原型链继承的属性。
◆方式 2:对象.hasOwnProperty('属性名'),返回布尔值,仅判断对象自身的属性,不包含继承属性,更精准。
◆方式3:通过判断属性名是否在 Object.keys() 返回的数组中,间接判断属性是否存在。

1
2
3
4
5
6
console.log('name' in obj1); // true
console.log(obj1.hasOwnProperty('age')); //true

let props = Object.keys(obj1);
console.log(props); // 先获取所有属性
console.log(props.includes('name')); // true

Array数组

  1. 什么是稀疏数组?
    如果数组中存在没有值的元素,该数组就是稀疏数组。
  2. 哪些方式可能会产生稀疏数组?
    ① 给数组添加新元素,索引与前面不连续。
    ② 使用 Array函数或构造函数方式创建数组,只有一个参数且是数字。
    ③ 修改数组的 length 属性,值比原来的大。

◆如果数组的元素还是数组,该数组可以称为多维数组

◆字符串具有一部分数组特性,有length属性,可以读取到字符串的长度; 但是length的值不能像数组一样修改。可以通过索引读取到某个字符,但是不能修改单个字符。字符串这种具有一部分数组特性但又不是数组的数据,统称为类数组(伪数组 Like-Array)

创建二维数组:

1
const ans = Array.from({ length: n }, () => Array(m));

数组的遍历

①普通 for 循环
for...in 循环

1
2
3
4
5
const arr = [10, 20, 30];

for (const index in arr) {
console.log(`索引 ${index}${arr[index]}`);
}

forEach(最常用,遍历所有元素,不可中断)

1
2
3
4
5
6
7
8
9
10
const arr = [10, 20, 30, 40];

arr.forEach((item, index) => {
console.log(`索引 ${index}${item}`);

// 注意:return 仅跳过当前次回调,无法中断整个循环
if (item === 20) {
return;
}
});

无法中断循环(break 报错,continue 无效,return 仅跳过当前次)。

④数组的内置方法

方法名 核心功能 示例
map 遍历数组,返回一个新数组(新数组长度与原数组一致,元素为回调函数返回值) const newArr = arr.map(item => item * 2);
filter 遍历数组,返回一个新数组(包含所有满足回调函数条件的元素) const newArr = arr.filter(item => item > 20);
find 遍历数组,返回第一个满足回调函数条件的元素(无满足条件的元素返回 undefined const target = arr.find(item => item === 30);
findIndex 遍历数组,返回第一个满足回调函数条件的元素索引(无满足条件的元素返回 -1 const targetIndex = arr.findIndex(item => item === 30);
every 遍历数组,判断所有元素是否都满足回调函数条件(返回布尔值) const isAllBig = arr.every(item => item > 0);
some 遍历数组,判断是否存在至少一个元素满足回调函数条件(返回布尔值) const hasBig = arr.some(item => item > 30);

数组的添加与删除

① 添加元素

1
2
3
4
5
6
7
8
9
10
1. 使用 数组.length 作为索引添加元素
数组[数组.length] = 新元素

2. push() 方法在数组后面添加一个或多个新元素

3. unshift() 方法在数组前面添加一个或多个新元素

4. splice() 方法指定位置添加一个或多个新元素
// 语法:splice(2, 0, 30)
// 解读:索引 2 开始,删除 0 个元素,插入 30

② 删除元素

1
2
3
4
1. 数组.length -= n;  删除后n个元素
2. 数组.pop() 删除最后一个元素,一次只能删除一个
3. 数组.shift() 删除第一个元素,一次只能删除一个
4. 数组.splice(索引,数量); 删除指定位置指定数量的元素

Function 函数

创建函数

① function 关键字方式

1
2
3
function 函数名(参数列表) {
语句...;
}

表达式方式

1
2
3
var 函数名 = function(参数列表) {
语句...;
}

◆关于return:

  • 当函数遇到return后,函数会立即停止执行并推出,因此return语句后面的代码不会执行。
  • return语句也可以不带返回值。这时候,函数会立即停止执行并返回undefined。这种用法最常用于提前终止函数执行,并不是为了返回值。

函数形参与实参

无默认值的参数是必传(调用时必须传入对应值,否则为 undefined),带默认值的参数是可选的,可选参数放在必选参数后面,符合正常的调用逻辑。

1
2
3
4
5
6
7
8
9
10
// 有默认值参数放在末尾
function sayHello(name, greeting = "你好") {
console.log(`${greeting}${name}!`);
}

// 调用场景1:传入所有参数(覆盖默认值)
sayHello("张三", "早上好"); // 早上好,张三!

// 调用场景2:只传入必选参数(默认值生效)
sayHello("李四"); // 你好,李四!

◆形参和实参的数量问题:

  1. 如果实参数量>形参数量,实参按照顺序给形参赋值,多出的实参没有作用。
  2. 如果实参数量<形参数量,实参按照顺序给形参赋值,后面的形参没有被赋值,使用的时候自动undefined。

◆arguments

  1. arguments 是系统创建的变量,只能在函数中使用。
  2. arguments 的值是一个伪数组,由调用函数时所传递的实参组成。
  3. 可以使用 arguments 实现可变参数数量的函数。
1
2
3
4
5
6
7
8
9
10
11
// 创建函数 该函数计算所有参数的和
function sum() {
// 定义变量 记录和
var res = 0;
// 遍历所有的参数
for (var i = 0; i < arguments.length; i ++) {
res += arguments[i];
}
// 返回计算结果
return res;
}

函数内的形参、argument 都是局部变量。

函数提升

  1. 全局代码执行之前会预处理, 查找全局代码中的function关键字,提前创建好变量并赋值完整函数体; 当正式执行到函数声明语句的时候,直接跳过。
  2. 函数调用的时候,执行函数体语句前也会预处理, 查找函数代码中的function关键字,提前创建好变量并赋值; 当正式执行到函数声明语句的时候,直接跳过。
1
2
3
4
5
6
7
8
9
function testFunc() {
console.log("1. 函数内执行前访问 funcVar:", funcVar);
var funcVar = "我是函数内变量";
console.log("2. 函数内赋值后访问 funcVar:", funcVar);
}

testFunc();

console.log("\n3. 函数外部访问 funcVar:", typeof funcVar);

运行结果:

  1. 函数内执行前访问 funcVar: undefined
  2. 函数内赋值后访问 funcVar: 我是函数内变量
  3. 函数外部访问 funcVar: undefined

回调函数

匿名函数就是没有名字的函数,匿名函数适合用于立即调用的函数和回调函数。

满足以下三个条件的函数就是回调函数:
1)函数是我定义的。
2)我没有调用(没有直接调用)。
3)函数最终执行了。

回调函数的使用场景:

  1. 数组的一些方法需要回调函数当参数,如 forEach、sort、filter、map、reduce 等等
  2. 定时器的回调函数
  3. DOM事件的回调函数
  4. Ajax 的回调函数
  5. Promise 的回调函数

    大部分回调函数的形式都是作为其他函数的参数!

构造函数

它是一个特殊的函数,专门用于批量创建具有相同结构和方法的对象,相当于对象的模板。每个对象都有对应的构造函数,不同数据类型的对象,构造函数不同。

  • 数组对象 → 构造函数 Array
  • 函数对象 → 构造函数 Function
  • 普通对象 → 构造函数 Object
  • 字符串对象 → 构造函数 String
1
2
3
4
5
const obj1 ={name:"March", age:18};

console.log(obj1.constructor === Object); // true
console.log(obj1 instanceof Array); // false
console.log(obj1.constructor); // [Function: Object]

◆构造函数与对象之间的关系

  1. 构造函数是模板。描述了对象的共同结构(有哪些属性、哪些方法)。
  2. 对象是构造函数的实例。是根据模板创建出来的具体对象,每个实例都拥有模板描述的属性和方法。

◆判断对象的构造函数

  1. instanceof 运算符,对象是否是某个构造函数的实例。
  2. constructor 属性,直接获取对象的构造函数。

实例化(创建对象的过程)
new 关键字调用构造函数,创建对象实例的过程,就叫实例化。new 构造函数(参数)。

  • {} → 等价于 new Object()
  • [] → 等价于 new Array()
  • 'hello' → 等价于 new String('hello')
    每实例化一次,就会创建一个全新的对象,每个对象都有独立的内存空间,互不影响。

自定义构造函数
规则:

  • 构造函数名首字母大写(约定俗成,区分普通函数)。
  • 函数内部用 this 关键字,指向即将被创建的对象实例,给 this 添加属性和方法,就是给实例添加属性和方法。
  • 必须用 new 关键字调用,否则 this 会指向全局(浏览器中是 window)。
1
2
3
4
5
6
7
8
9
10
11
12
function User(name, age, address){
    this.name = name;
    this.age = age;
    this['home-address'] = address;
    this.buy = function(item){
        console.log(`${this.name} 买了一件商品:${item}`);
    }
}

let user1 = new User('March', 18, '四川达州');
console.log(user1);
user1.buy('ViVO X100s Pro');

原始类型数据的对象特性
1.原始类型(Number、String、Boolean)的两种状态。

  • 值状态:直接量创建(如 var num = 10;),存储的是原始值,占用内存小,是默认状态。
  • 对象状态(包装对象):用 new 关键字创建(如 var numObj = new Number(10);),存储的是对象,占用内存大,拥有构造函数的原型方法(如 toString()length)。
    2.自动转换(隐式包装)
  • 当对值状态的原始类型调用对象方法(如 num.toString())时,JavaScript 会自动将其包装为对象状态,执行方法后再销毁包装对象,恢复为值状态。这就是为什么原始值可以调用对象方法(如 'hello'.length),我们无需手动创建包装对象。

Date 日期

要创建日期对象,就使用new操作符来调用Date构造函数:

1
let now = new Date();

在不给Date构造函数传参数的情况下,创建的对象将保存当前日期和时间。

指定日期创建时,使用new Date()构造函数时需明确指定目标日期时间,参数格式为(年,月,日,时,分,秒),注意,月份参数从0开始计数,1表示二月。
时分秒参数未指定时默认为0(可省略不写)。

计算日期时间差:

  • 时间戳原理:通过getTime()方法获取1970年1月1日至今的毫秒数。
  • 差值计算:目标日期时间戳减去当前日期时间戳得到剩余毫秒数。
  • 动态更新:需要在定时器回调函数内重新计算当前时间。

将seconds转换成x天x时x分x秒:

1
2
3
4
5
6
7
var days = Math.floor(seconds / (1000 * 60 * 60 * 24));

var hours = Math.floor((seconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));

var mins = Math.floor((seconds % (1000 * 60 * 60)) / (1000 * 60));

var secs = Math.floor((seconds % (1000 * 60)) / 1000);

倒计时案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
(function(){
var box = document.querySelector('#box');
// 创建目标日期时间对象 2024-02-10
var dstDate = new Date(2024, 1, 10);
// 计算倒计时
runTime();
    // 设置定时器 时间间隔1000ms
var intervalId = setInterval(runTime, 1000);
    // 计算倒计时的函数
function runTime() {
// 创建当前的日期时间对象
var currDate = new Date();
// 计算目标日期与当前日期相差的毫秒数
var seconds = dstDate.getTime() - currDate.getTime();
// 如果到达了目标日期
if (seconds <= 0) {
clearInterval(intervalId);
box.innerHTML = '春节快乐!';
return;
}
// 从相差的毫秒数中提取整的天数
var d = Math.floor(seconds / (24*3600000));
// 提取整的小时数 先取出不足一天的毫秒数 除以3600000
var h = Math.floor(seconds % (24*3600000) / 3600000);
// 提取整的分钟数
var i = Math.floor(seconds % 3600000 / 60000);
// 提取剩下的不足一分钟的毫秒数
var s = Math.floor(seconds % 60000 / 1000);
// 个位数补 0
d = d < 10 ? '0' + d : d;
h = h < 10 ? '0' + h : h;
i = i < 10 ? '0' + i : i;
s = s < 10 ? '0' + s : s;
// 拼接字符串
box.innerHTML = '距离春节还有<br>'+d+'天'+h+'小时'+i+'分钟'+s+'秒';
}
})();

原始值与引用值

在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。
1.保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。
2.引用值是保存在内存中的对象。对于引用值而言,可以随时添加、修改和删除其属性和方法。

💡注意,原始类型的初始化可以只使用原始字面量形式。如果使用的是new关键字,则JavaScript会创建一个Object类型的实例,但其行为类似原始值。

1
2
3
4
5
6
7
8
    let name1 = "Nicholas";
    let name2 = new String("Matt");
    name1.age = 27;
    name2.age = 26;
    console.log(name1.age);     // undefined
    console.log(name2.age);     // 26
    console.log(typeof name1); // string
    console.log(typeof name2); // object

◆复制值:除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
1.在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。它们独立使用,互不干扰
2.在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来

◆传递参数:ECMAScript中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。

1
2
3
4
5
6
7
8
    function addTen(num) {
      num += 10;
      return num;
    }
    let count = 20;
    let result = addTen(count);
    console.log(count);   // 20,没有变化
    console.log(result); // 30

💡如果num是按引用传递的,那么count的值也会被修改为30。

◆关于栈内存堆内存
引用类型参数的值是栈内存里面的「堆内存地址」,这是容易混淆的核心
图源:March

下面这个例子,如果person是按引用传递的,那么person应该自动将指针改为指向name为”marchfood”的对象。
‼️实际情况:当我们再次访问person.name时,它的值是 “March” ,这表明函数中参数的值改变之后,原始的引用仍然没变。当你执行 obj = new Object() 时,obj 就抛弃了原来的副本地址,转而指向函数内部新建的一个临时对象;这个临时对象只存在于函数执行的过程中,函数执行完后,JavaScript 垃圾回收机制会把它销毁。

1
2
3
4
5
6
7
8
9
    function setName(obj) {
      obj.name = "March";
      obj=new Object();
      obj.name="marchfood";
    }

    let person = new Object();
    setName(person);
    console.log(person.name);   // "March"

而之前修改的堆对象并不会消失,外部 person 依然牢牢指向它,这就是为什么 name 还是 March,而不是 undefined

确定类型

前面学过typeof操作符最适合用来判断一个变量是否为原始类型。更确切地说,它是判断一个变量是否为字符串、数值、布尔值或undefined的最好方式。如果值是对象或null,那么typeof返回”object”。

typeof虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象,而是想知道它是什么类型的对象。

1
2
    console.log(person instanceof Object);   // 变量person是Object吗?
    console.log(colors instanceof Array);    // 变量colors是Array吗?

如果用instanceof检测原始值,则始终会返回false,因为原始值不是对象。

第二个操作数是对象原型链上的某个对象的构造函数也成立。

垃圾回收机制

JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。通过自动内存管理实现内存分配和闲置资源回收。

基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。

垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。

比如,函数。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。

垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量有不同的实现方式。在浏览器的发展史上,用到过两种主要的标记策略:标记清理引用计数

标记清理
垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)​。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。

引用计数
其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。

这种策略很快就遇到了严重的问题:循环引用。所谓循环引用,就是对象A有一个指针指向对象B,而对象B也引用了对象A。

运算符

=== 操作数的类型和值都相等才是true。(首选)
== 先尝试将两个操作数转换成相同类型,然后进行值比较。

转换规则:

  • 数字和字符串比较:字符串转换成数字,再比较。
  • 布尔值和其他类型比较:布尔值转换成数字(true → 1false → 0),再比较。
  • nullundefined 比较:返回 true(彼此相等,且不等于其他任何值)。

关于自增自减:

1
2
3
4
5
6
7
var n = 100;

console.log(n ++ && n --);  //101
console.log(n);             //100 

console.log(-- n || n ++);  //100
console.log(n); //99

常见语句

if同c语言
for同c语言
switch同c语言:

1
2
3
4
5
6
7
switch (表达式) {
case 表达式可能的值: 语句...; break;
case 表达式可能的值: 语句...; break;
case 表达式可能的值: 语句...; break;
case 表达式可能的值: 语句...; break;
default: 语句...;
}

关键字 this

  1. 在函数外面使用(全局下使用):Node.js 中全局对象是 globalThis,浏览器中是 window
  2. 在构造函数内部使用:this 的值是构造函数的实例(实例化构造函数所创建的对象)。
  3. 在函数(方法)中使用:this 的值是调用该函数(方法)的对象。
    注意:不要看函数声明语句所在的地方,看调用函数的语句,看.前面是哪个对象。

关于window:

  1. window 表示浏览器窗口, 运行在浏览器上的js,window 作为全局对象。
  2. 在打开浏览器的时候 window 对象就自动创建了。
  3. 所有的全局变量都是 window 的属性, 使用 window 的属性可以省略 window。

内置对象

ECMA-262对内置对象的定义是“任何由ECMAScript实现提供、与宿主环境无关,并在ECMAScript程序开始执行时就存在的对象”​。

其实前面已经接触了大部分内置对象,包括Object、Array和String。

Global

Global对象是ECMAScript中最特别的对象,因为代码不会显式地访问它。ECMA-262规定Global对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成Global对象的属性。比如isNaN()、isFinite()、parseInt()和parseFloat(),实际上都是Global对象的方法。除了这些,Global对象上还有另外一些方法。

URL编码方法
encodeURI()和encodeURIComponent()方法用于编码统一资源标识符(URI)​,以便传给浏览器。这两个方法的主要区别是,encodeURI()不会编码属于URL组件的特殊字符,比如冒号、斜杠、问号、井号,而encodeURIComponent()会编码它发现的所有非标准字符。

1
2
3
4
    let uri = "http://www.wrox.com/illegal value.js#start";
   
    console.log(encodeURI(uri)); // "http://www.wrox.com/illegal%20value.js#start"
    console.log(encodeURIComponent(uri)); // "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"

前者会将空格被替换为%20。

eval()方法
通过eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在eval()执行的时候才会被创建。
💡在严格模式下,在eval()内部创建的变量和函数无法被外部访问。

Math

ECMAScript提供了Math对象作为保存数学公式、信息和计算的地方。Math对象提供了一些辅助计算的属性和方法。

◆Math对象属性

属性 说明
Math.PI 圆周率

◆min()和max()方法
min()和max()方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数。

1
2
3
    let values = [1, 2, 3, 4, 5, 6, 7, 8];
    let max = Math.max(...values);
    console.log(max); // 8

◆舍入方法

  • Math.ceil()方法始终向上舍入为最接近的整数。
  • Math.floor()方法始终向下舍入为最接近的整数。
  • Math.round()方法执行四舍五入

◆random()方法
Math.random()方法返回一个01范围内的随机数,其中包含0但不包含1。
例如,从110范围内随机选择一个数:

1
let num = Math.floor(Math.random() * 10 + 1);

原型与原型链

原型

原型的概念与特点

每个函数都会创建一个prototype属性,这个属性是一个Object对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。原型本身也是普通对象,对象可以直接借用原型上的属性 / 方法(这个过程叫原型继承)。

◆原型特点:

  1. 每个对象都有原型。
  2. 原型也是对象。
  3. 对象可继承原型属性。

使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
    function Person() {}
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function() {
      console.log(this.name);
    };

    let person1 = new Person();
    person1.sayName(); // "Nicholas"
    let person2 = new Person();
    person2.sayName(); // "Nicholas"
    console.log(person1.sayName == person2.sayName); // true

分析:这里,所有属性和sayName()方法都直接添加到了Person的prototype属性上,构造函数体中什么也没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此person1和person2访问的都是相同的属性和相同的sayName()函数。

如何获取对象的原型?

  1. 隐式:通过对象获取原型。
  2. 显式:通过对象的构造函数获取原型。
  3. 使用Object的方法。
1
2
3
4
5
6
7
8
9
10
11
console.log(person1.__proto__);
console.log(Person.prototype);
console.log(Object.getPrototypeOf(person1));
// {
//   name: 'Nicholas',
//   age: 29,
//   job: 'Software Engineer',
//   sayName: [Function (anonymous)]
// }

console.log(Person.prototype.constructor); //[Function: Person]

默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。

对象、构造函数、原型之间的关系

① 对象和构造函数

  1. 构造函数是对象的描述,对象是构造函数的实例。
  2. 一个构造函数可以有无数个对象,一个对象只能有一个构造函数。

② 对象和原型

  1. 每个对象都有原型,可以使用原型上的属性。
  2. 一个对象只能有一个原型,一个原型可以作为多个对象的原型。

③ 构造函数和原型

  1. 可以通过构造函数获取到对象的原型。
  2. 构造函数相同的对象,原型也是相同的; 相同数据类型的原型,原型相同。
1
2
3
4
5
6
7
8
9
// 不同数组
const arr1 = [1,2,3];
const arr2 = new Array(4,5,6);
const arr3 = []; // 空数组

// 验证原型是否相同
console.log(Object.getPrototypeOf(arr1) === Object.getPrototypeOf(arr2)); // true
console.log(Object.getPrototypeOf(arr1) === Object.getPrototypeOf(arr3)); // true
console.log(Object.getPrototypeOf(arr1) === Array.prototype); // true(最终都指向 Array.prototype)

◆判断属性是否属于对象本身

1
对象.hasOwnProperty('属性名');

只有属性在对象本身上才返回true,否则都是false(即使在原型不在本身也是false)。

创建对象的同时设置原型

1.var obj1 = {}:是创建普通对象的字面量语法,默认原型是 Object.prototype(JavaScript 所有普通对象的默认原型);
2.Object.create(proto):ES5 提供的自定义原型创建对象的方法,核心作用是:

  • 创建一个空对象(自身无任何属性);
  • 将这个空对象的 [[Prototype]](原型)强制设置为传入的 proto 参数
  • 如果 protonull,则创建无原型的纯净对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建对象 原型是提取准备好的 实例化的时候将对象与原型关联
var obj1 = {};
console.log(obj1);
console.log('');

// 创建对象的同时 自己设置原型
var obj2 = Object.create([10,20,30,40]);
console.log(obj2);
console.log('');

// 创建对象的同时 自己设置原型
var obj3 = Object.create(new String('hello'));
console.log(obj3);
console.log('');//最终:obj3自身空,继承字符串对象的所有属性或方法。

// 创建没有原型的对象
var obj4 = Object.create(null);
console.log(obj4);

原型链

每个对象都有原型,原型还是个对象,原型也有原型,原型的原型也有原型,组成了原型链。原型链终点是Object.prototype,其__proto__为null。

1
2
3
var arr1 = [0,1,2,3];
//arr1.__proto__ === Array.prototype
//arr1.__proto__.__proto__ === Object.prototype

◆作用:

  1. 对象在查找找属性的时候,先从自身去找看有没有这个属性,如果有,直接使用这个属性的值。
  2. 如果没有,会沿着原型链向上找,如果找到就使用这个属性的值且停止查找,如果没找到继续向上找直到原型链的终点。
  3. 如果找到原型链的终点还没有找到,就返回 undefined 。

题目思考

  1. 下面代码中的对象 f 有方法 a 和方法 b 吗?
1
2
3
4
5
var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}

var f = new F()
  • 实例 f 的原型链(普通对象)f → F.prototype → Object.prototype → null
  • 函数 F 的原型链(函数对象)F → Function.prototype → Object.prototype → null

其他

绝对值:Math.abs()
平方:Math.pow()
toString() 转化为字符串