JavaScript基础知识总结(四):常见内置构造函数,正则表达式,作用域与闭包

9 阅读15分钟

一:常见内置对象与内置构造函数

1.常见内置对象

(1) Math

Math.random(); // 0~1之间
Math.round(4.6); // 5
Math.max(1, 5, 3); // 5

(2) Date

let now = new Date();
console.log(now.getFullYear(), now.getMonth() + 1, now.getDate());

(3) JSON

let obj = { a: 1, b: 2 };
let str = JSON.stringify(obj);
let newObj = JSON.parse(str);

2.常见内置构造函数

js几乎所有数据类型都可以使用对象的方式来操作,在底层将简单数据类型包装成了复杂数据类型 (包装成了对象)

1.常见的构造类型

Object Array String Number Boolean Function Date Math RegExp Error

2.Object构造函数

1.构造函数
//Object是内置的构造函数 用来创建对象的
const obj=new Object()
obj.name='peiqi'
obj.age=18
obj.sayHi=function(){
console.log('hi');
}
2.具体静态方法

常用静态方法: Object.keys() Object.values() Object.assign()

1.Object.keys() 返回一个数组 包含对象自身的所有属性名

console.log(Object.keys(obj)) //['name', 'age', 'sayHi']数组的形式
Object.values() 返回一个数组 包含对象自身的所有属性的值
console.log(Object.values(obj)) //['peiqi', 18, ƒ]

2.Object.assign() 拷贝对象的所有属性到指定对象中 返回一个新对象 或者为对象添加属性(合并对象)

const obj2=Object.assign({},obj)
console.log(obj2); //{name: 'peiqi', age: 18, sayHi: ƒ}
Object.assign(obj,{gender:'男'})
console.log(obj); //{name: 'peiqi', age: 18, sayHi: ƒ, gender: '男'}

3. Array构造函数

1.构造函数:

一般使用 字面量[ ]直接创建数组,使用构造函数会有一些麻烦

const arr = new Array() 构造数组函数,但平时不常用 因为[]更方便
2.具体的静态方法
  • forEach只遍历数组,但不返回新数组

    arr.forEach(function(item,index){
      console.log(item,index); //1 0 2 1 3 2 4 3 5 4 6 5 7 6 8 7 9 8 10 9 
    })
    
  • filter 筛选数组并且返回一个新数组

    const arr2=arr.filter(function(item,index){
      return item>=6 
    })
    console.log(arr2); //[6, 7, 8, 9, 10]
    
  • map遍历数组并且返回一个新数组

    • 注意使用map方法去遍历一个对象数组的同时,直接修改对象的属性会影响到元素组 因为这里的对象是一个引用类型的数据

      // 创建一个包含对象的数组
      let lesson = [
        {title: 'css学习', body: 'css'},
        {title: 'html学习', body: 'html'},
        {title: 'JS学习', body: 'js'}
      ];
      
      // 当我们使用 map 时,item 变量得到的是原对象的引用
      let modifiedLesson = lesson.map(item => {
        // 这里直接修改 item.title 实际上是在修改原对象
        item.title += '课程'; 
        return item;
      });
      
      console.log(lesson[0].title); // 输出: "css学习课程"
      console.log(modifiedLesson[0].title); // 输出: "css学习课程"
      // 可以看到,原数组也被修改了!
      
      内存中的情况:
      lesson 数组: [地址1, 地址2, 地址3]
                   ↓      ↓      ↓
      对象内容:   {title: "css学习", body: "css"}
                 {title: "html学习", body: "html"}
                 {title: "JS学习", body: "js"}
      
      当执行 map 时:
      item 变量: → 地址1 (指向第一个对象)
      执行 item.title += '课程' 实际上是修改了地址1指向的对象
      因此原数组中的对象也被修改了
      

      正确处理流程:

      let lesson = [
        {title: 'css学习', body: 'css'},
        {title: 'html学习', body: 'html'},
        {title: 'JS学习', body: 'js'}
      ];
      
      // 使用展开运算符创建新对象
      let modifiedLesson = lesson.map(item => {
        return {
          ...item,              // 复制原对象的所有属性
          title: item.title + '课程'  // 创建新的 title 属性值
        };
      });
      
      console.log(lesson[0].title); // 输出: "css学习" (未被修改)
      console.log(modifiedLesson[0].title); // 输出: "css学习课程"
      
    const arr3=arr.map(function(item,index){
      return item*2 
    })
    console.log(arr3); //[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
    
  • reduce数组累计器,一般用于数组的求和,有时候会自带初始值

    const total=arr.reduce(function(prev,cur){
      // prev 上一次的返回值 cur 当前遍历的元素
      return prev+cur
    })
    console.log(total); //55
    const total2=arr.reduce(function(prev,cur){
        return prev+cur
    },10)//这里的10就prev的初始值 
    console.log(total2); //65
    
  • sort数组排序,会改变原数组''

    const arr4=[1,2,3,4,5,6,7,8,9,10]
    arr4.sort((a,b)=>{
      return a-b
    })
    console.log(arr4); //[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
    
  • reverse 反转数组,会改变原数组

    const arr4=[1,2,3,4,5,6,7,8,9,10]
    arr4.reverse()
    console.log(arr4); //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
  • concat拼接数组,不会改变原数组,返回一个新数组

    const arr4=[1,2,3,4,5,6,7,8,9,10]
    const arr5=arr4.concat([11,12,13])
    console.log(arr5); //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
    
  • slice截取数组,不会改变原数组

    const arr6=arr5.slice(0,5)
    console.log(arr6); //[1, 2, 3, 4, 5]
    
  • slice数组替换,会改变原数组 返回一个新数组

    const arr8=[1,2,3,4,5,6,7,8,9,10]
    const arr9=arr8.splice(0,5)
    console.log(arr9); //[1, 2, 3, 4, 5]
    console.log(arr8); //[6, 7, 8, 9, 10]
    
  • indexOf 返回数组中第一个满足条件的元素的索引 如果没有找到返回-1

    const arr7=[1,2,3,4,5,6,7,8,9,10]
    const index=arr7.indexOf(5)
    console.log(index); //4
    
  • join不会改变原数组 返回一个新数组

    const arr14=[1,2,3,4,5,6,7,8,9,10]
    const arr15=arr14.join('-')
    console.log(arr15); //1-2-3-4-5-6-7-8-9-10
    
  • every 会改变原数组 返回一个新数组 判断数组中是否所有元素都满足条件

    const arr16=[1,2,3,4,5,6,7,8,9,10]
    const arr17=arr16.every(item=>item>5)
    console.log(arr17); //false
    
  • some 会改变原数组 返回一个新数组 判断数组中是否有一个元素满足条件

    const arr18=[1,2,3,4,5,6,7,8,9,10]
    const arr19=arr18.some(item=>item>5)
    console.log(arr19); //true
    
  • find 会改变原数组 返回一个新数组 找到第一个满足条件的元素 返回该元素 如果没有找到返回undefined

    const arr20=[1,2,3,4,5,6,7,8,9,10]
    const arr21=arr20.find(item=>item>5)
    console.log(arr21); //6
    
  • from 会改变原数组 返回一个新数组 将一个伪数组对象转换成数组

4.String构造函数

1.构造函数:
// 使用 String 构造函数将值转换为字符串
let num = 123;
let str = new String(num); // 创建一个 String 对象
console.log(typeof str);   // 输出: object
console.log(str);          // 输出: "123"

// 更常见的做法是直接调用 String 函数(不使用 new)
let simpleStr = String(num);
console.log(typeof simpleStr); // 输出: string
console.log(simpleStr);        // 输出: "123"
2.具体的静态方法
  • .length属性 数组的长度 数组的长度是数组中元素的个数

  • split('分隔符') 字符串的方法 把字符串分割成数组 返回一个新数组

    const str='pink,jiong,red'
    const arr=str.split(',')
    console.log(arr); //['pink', 'jiong', 'red']
    
  • substring(start,end) 字符串的方法 截取字符串 返回一个新字符串

     //start 开始的位置 end 结束的位置 不包括end 可以省略 省略表示到末尾
    const str2='pinkjiongred'
    const str3=str2.substring(0,4)
    console.log(str3); //pink
    
  • startsWith('字符串',参数)字符串的方法 判断字符串是否以指定字符串开头 返回一个布尔值

    //参数 可选 从哪个位置开始判断 默认从0开始 注意是从参数(包含参数)开始的位置
    const str4='pinkjiongred'
    const str5=str4.startsWith('pink')
    console.log(str5); //true
    
  • includes('字符串',参数) 字符串的方法 判断字符串是否包含指定字符串 返回一个布尔值

    //参数 可选 从哪个位置开始判断 默认从0开始 注意是从参数(包含参数)开始的位置
    const str6='pinkredgreen'
    const str7=str6.includes('jiong',0)
    console.log(str7); //false 只要包含就行,不需要考虑位置
    
  • toUpperCase() 字符串的方法 把字符串转换成大写 返回一个新字符串

  • toLowerCase() 字符串的方法 把字符串转换成小写 返回一个新字符串


二:类型转换

(1) 显式转换

Number('123'); // 123
String(123);   // "123"
Boolean(0);    // false

(2) 隐式转换

console.log('5' - 1); // 4(字符串转数字)
console.log('5' + 1); // "51"(数字转字符串)

三:正则表达式

正则表达式 适用于匹配字符串组合的模式 js正则表达式也是对象,通常用来查找 替换 哪些符合正则表达式的文本,许多语言都支持正则表达式

1.定义方法:

const  变量名 = /表达式/

2.查找是否匹配:(使用reg.text(字符串) 的方法)

const str=" 你好,世界"
//定义规则
const reg= /你好/
//text()方法 用来查看表达式与字符串是否匹配 返回true false
console.log(reg.test(str));
//esec()方法 在一个指定字符串中执行一个搜索匹配 如果匹配成功返回一个数组,返回失败是NUll
console.log(reg.exec(str));

3.元字符

元字符是一种特殊字符提高了灵活性和匹配功能.

const regex1 = /\d+/;  // 匹配一个或多个数字
const regex2 = /^[A-Z]/; // 匹配以大写字母开头的字符串
const regex3 = /\w{4,}/; // 匹配至少4个字母/数字/下划线

具体方法:

  1. 边界符号 ^ :用来界定字符所处的位示匹配行首的文本(以谁开始): 用来界定字符所处的位置 ^ 表示匹配行首的文本(以谁开始) 表示匹配行尾的文本(以谁结束)

    • // 示例1:匹配以"Hello"开头的字符串
      console.log(/^Hello/.test("Hello World")); // true
      console.log(/^Hello/.test("Say Hello"));   // false
      
      // 示例2:匹配以"World"结尾的字符串
      console.log(/World$/.test("Hello World"));  // true
      console.log(/World$/.test("World Peace"));  // false
      
      // 示例3:精确匹配整个字符串
      console.log(/^Hello World$/.test("Hello World")); // true
      console.log(/^Hello World$/.test(" Hello World ")); // false
      
  2. 量词: 表示重复次数 设定某个模式(规则)出现的次数

    • // * 匹配前一个字符0次或多次 如果加入精确匹配,只允许匹配的字符出现 如果出现其他字符就是false
      console.log(/ab*c/.test("ac"));     // true (b出现0次)
      console.log(/ab*c/.test("abc"));    // true (b出现1次)
      console.log(/ab*c/.test("abbbc"));  // true (b出现多次)
      
      // + 匹配前一个字符1次或多次 >=1
      console.log(/ab+c/.test("ac"));     // false (b至少出现1次)
      console.log(/ab+c/.test("abc"));    // true
      console.log(/ab+c/.test("abbbc"));  // true
      
      // ? 匹配前一个字符0次或1次 0||1
      console.log(/ab?c/.test("ac"));     // true (b出现0次)
      console.log(/ab?c/.test("abc"));    // true (b出现1次)
      console.log(/ab?c/.test("abbbc"));  // false (b出现多次)
      
      // {n} 匹配前一个字符恰好n次   ==n
      console.log(/a{3}/.test("aaa"));    // true
      console.log(/a{3}/.test("aa"));     // false
      
      // {n,} 匹配前一个字符至少n次   >=n
      console.log(/a{2,}/.test("aa"));    // true
      console.log(/a{2,}/.test("aaa"));   // true
      console.log(/a{2,}/.test("a"));     // false
      
      // {n,m} 匹配前一个字符n到m次  >=n&& <=m
      console.log(/a{2,4}/.test("aa"));   // true
      console.log(/a{2,4}/.test("aaa"));  // true
      console.log(/a{2,4}/.test("a"));    // false
      console.log(/a{2,4}/.test("aaaaa")); // false
      
  3. 字符类型: [a-z]26个英文字母 [a-zA-Z]大小写都行 [ ] 匹配方括号中的任意一个字符 可以添加量词允许重复

    • // 示例1:匹配a或b或c
        console.log(/^[abc]$/.test("a"));      // true (精确匹配单个字符)
        console.log(/^[abc]{2}$/.test('ab'));  // true (精确匹配两个字符)
        console.log(/^[abc]{2}$/.test('a'));   // false (不足两个字符)
        console.log(/^[abc]{2}$/.test('abc')); // false (超过两个字符)
      
      
  4. 元字符的预定义

    • //预定义是某些常见模式的简写方式
      // \d 匹配0-9之间的任意数字 [等价于[0-9]]
      // \D 匹配0-9以外的任意字符 [等价于[^0-9]]
      // \w 匹配字母、数字和下划线 [等价于[a-zA-Z0-9_]]
      // \W 匹配非字母、数字和下划线的字符 [等价于[^a-zA-Z0-9_]]
      // \s 匹配空格(包括换行符,制表符,空格符) [\t\r\n\v\f]
      // \S 匹配非空格(包括换行符,制表符,空格符) [^\t\r\n\v\f]
      // ^\d{4}-\d{1,2}-\d{1,2}$
      
  5. 修饰符

    • //修饰符约束正则执行的某些细节行为,如是否区分大小写,是否支持多行匹配等
      // /表达式/修饰符
      // i 匹配时不区分大小写
      // g 匹配时,匹配所有满足正则表达式的结果
      console.log(/a/i.test('A')); //true
      
      //replace替换 找到所需要的文本替换
      const str='java是我要学的语言,但是我不想学JAVA'
      const re=str.replace(/java/ig,'js');
      console.log(re);
      

四: 作用域与闭包

(1)作用域

1.环境与作用域

1)执行环境

环境的存在价值就是被需要,全局环境不会被回收,比如创建一个函数,每次调用函数都会创建一个函数环境就,函数执行完之后这个环境就会被销毁。函数执行多次,里面的内存地址也会被声明多个内存地址,执行完一次后,里面的内存地址被清理掉

  • 全局环境:代码开始执行时创建,程序退出前不会被回收
  • 函数环境:每次函数调用都会创建新的执行环境,函数执行完毕后该环境被销毁
  • 环境就是系统执行代码遵循的运行规则->决定代码的生命周期与实际值
2)作用域
  • 全局作用域:在代码任何地方都能访问的变量和函数
  • 局部作用域:在特定函数内部声明的变量和函数,只能在该函数内部访问
  • 作用域是编写代码的词法规则->决定变量在哪可以被访问

简单总结:环境就是系统执行代码遵循的运行规则->决定代码的生命周期与实际值,作用域是编写代码的词法规则->决定变量在哪可以被访问

3)延长环境的生命周期

假如函数中是一个自增的n,那么该环境的生命周期就是 函数被调用-->环境被创建-->n自增-->执行完毕内存中环境销毁;当多次调用该函数n是不会一直自增的

function hd(){
    let n=0;
    function sum(){
        console.log(++n);
    }
    sum();
}
hd();
hd();

如果想延长这个生命周期,就需要将sum函数返回出去使得外部存在sum的引用 这样就可以延长环境的生命周期

function hd1(){
    let n=0;
    function sum(){
        console.log(++n);
    }
    return sum;
}
let fn=hd1();
fn();
fn();

延长构造函数中的生命周期

function hd(){
    let n=0;
    this.sum=function(){
        console.log(++n);
    }
}
let a=new hd();
a.sum();
a.sum();
5) let var在for循环中的不同效果

使用var声明for中的i

for (var i = 1; i <= 3; i++) {
    // 因为var是全局作用域,每次循环都是把上次的结果改变,倒计时1秒后已经循环完毕,循环完毕的结果就是4
    // 因为循环了三次,所以打印出3个4
    // 结果为4的原因是第四循环时已经把i变成了4,但是没有通过 i <= 3的判断条件
    // 但此时i的值已经变成了4,所以打印结果就是4
    setTimeout(() => {
      console.log(i); // 三个4
    }, 1000);
  }
 for (let a = 1; a <= 3; a++) {
    // 因为let是块级元素,每次循环都会产生一个新的块级作用域,每个作用域里面存着每次循环的结果
    // 倒计时1秒后吧每个块里面的元素打印出来,所以就是 1 2 3
    setTimeout(() => {
      console.log(a); // 1 2 3
    }, 1000);
 }

利用自执行函数改造var

for (var a = 0; a <= 5; a++) {
    // 自执行函数,每次循环都产生一个新的函数作用域,每个函数作用域存着这次循环的结果
    // 倒计时1秒后打印出每个函数作用域里面的值
    (function(a) {
      setTimeout(() => {
        console.log(a);
      }, 1000);
    })(a);
  }

(2)闭包

1.概念:

闭包是指一个函数能够访问并“记住”其外部作用域中的变量,即使在外层函数已经执行完毕后,内层函数仍然可以访问这些变量。

简单理解就是里层函数用到了外层的变量 就会被捆绑在一起作为闭包

2.形成条件:

  • 在函数中存在一个内部函数
  • 内部函数引用了外部函数里面的变量
  • 外部函数返回这个内部函数

3.核心机制:

当一个函数被创建时,它会记住自己被定义时的环境(即词法作用域),这样即便在不同的执行上下文中调用该函数,它依然能访问到原始作用域中的变量。

4.常规用途:

  • 创建私有变量(模拟封装)
  • 实现数据缓存或记忆功能
  • 在异步操作、回调函数中保持状态

5.注意事项:

  • 如果你不小心创建了很多闭包,或者长时间持有不必要的引用,可能会导致内存泄漏
  • 因此,在不需要的时候应该手动解除引用(例如将函数设为 null)以帮助垃圾回收。
  • this在闭包中也存在问题 因为闭包会造成对象中的方法的this没有指向 所以在对象中如果使用闭包 返回的函数为箭头函数会好一些
  • let hd={
        name:'后盾人',
        getName:function(){
            return function(){
                return this.name
            }
        }
    }
    console.log(hd.getName()());//undefined
    //因为我么调用getname后返回一个函数 这个函数中的this是没有指向的 也就是直接指向了window 
    
    let hd2={
        name:'后盾人',
        getName:function(){
            return ()=>{
                return this.name
            }
        }
    } 
    //箭头函数没有自己的this 他的this是继承的上层作用域的this
    //这就是解决方法
    console.log(hd2.getName()());//undefined
    
    

6.实例代码:

/**
 * 闭包示例:
 * - outerFunction 执行后返回 innerFunction
 * - innerFunction 保留对 outerVariable 的引用
 * - 即使 outerFunction 执行完毕,outerVariable 也不会被回收
 * - 这就是闭包实现私有变量的原理
 */

//一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
//闭包=内层函数+外层函数的变量
function outerFunction(x) {
    // 外层函数的局部变量
    let outerVariable = x;

    // 返回一个内层函数
    return function innerFunction(y) {
        // 内层函数使用了外层函数的变量
        console.log(outerVariable + y);
    };
}

const closureExample = outerFunction(10);
closureExample(5); // 输出 15

问题总结与回答:

1.为什么可以对外层函数的局部变量进行私有化?

比如在示例代码中,outerFunction() 是一个外层函数,其中定义了一个局部变量 outerVariable,该变量被内层函数 innerFunction() 所引用。这是一种典型的闭包引用关系。

尽管 outerFunction() 已经执行完毕,按理说其内部变量应当被垃圾回收机制清理掉,但由于 outerFunction()innerFunction() 返回并赋值给了外部环境中的变量 closureExample,这就使得 innerFunction() 依然存活在内存中。

由于 innerFunction() 依赖于 outerVariable,而 innerFunction() 又依赖于全局变量 closureExample 的存在,而全局变量不会被垃圾回收器回收,因此 outerVariable 也被保留在内存中,不会被释放。

这一过程实际上实现了变量的“私有化”——外部无法直接访问 outerVariable,只能通过闭包暴露的接口(即 closureExample)间接操作它。