JS进阶技巧

1.作用域

作用域链

作用域链本质上是底层的变量查找机制

  1. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域

垃圾回收机制

JavaScript主要采用两种垃圾回收算法:标记-清除(Mark-and-Sweep)和引用计数(Reference Counting)。

标记-清除(Mark-and-Sweep)

  1. 标记阶段:垃圾回收器首先遍历所有可达的对象(即从根对象开始,通过引用关系可以访问到的所有对象),并将它们标记为“活着”。根对象包括全局变量、执行上下文中的变量、函数调用栈中的变量等。
  2. 清除阶段:遍历完所有可达对象后,垃圾回收器会再次遍历堆内存中的所有对象,未被标记的对象被视为不再使用,其占用的内存将在这一阶段被回收。

引用计数(Reference Counting)

这是一种更简单的垃圾回收策略,每个对象都有一个引用计数,每当有一个新的引用指向该对象时,引用计数加1;当引用被删除或失去作用域时,引用计数减1。当一个对象的引用计数降为0时,表示没有变量再引用它,此时就可以直接释放该对象所占用的内存。

优缺点:

  • 标记-清除的优点在于能够处理循环引用的问题,而引用计数机制则无法处理这种情况。但是,标记-清除算法在执行时会暂停其他脚本执行,可能导致应用暂时失去响应(即所谓的“全停顿”)。
  • 引用计数简单高效,但存在循环引用的问题,即两个对象相互引用但不再被外部访问时,它们的引用计数不会降为0,导致内存泄漏。

闭包

闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量,可以创建私有变量,避免全局污染。

// 闭包的写法  统计函数的调用次数
    function outer() {
      let count = 1
      function fn() {
        count++
        console.log(`函数被调用${count}`)
      }
      return fn
    }
    const re = outer()
    // const re = function fn() {
    //   count++
    //   console.log(`函数被调用${count}次`)
    // }
    re()
  • 闭包 = 内层函数 + 外层函数的变量

但是存在内存泄露的问题

2.函数

默认值

<script>
  // 设置参数默认值
  function sayHi(name="小明", age=18) {
    document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
  }
  // 调用函数
  sayHi();
  sayHi('小红');
  sayHi('小刚', 21);
</script>

总结:

  1. 声明函数时为形参赋值即为参数的默认值
  2. 如果参数未自定义默认值时,参数的默认值为 undefined
  3. 调用函数时没有传入对应实参时,参数的默认值被当做实参传入

动态参数arguments

arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。

arguments 的作用是动态获取函数的实参

<script>
  // 求和函数,计算所有参数的和
  function sum() {
    let s = 0
    for(let i = 0; i < arguments.length; i++) {
      s += arguments[i]
    }
    console.log(s)
  }
  // 调用求和函数
  sum(5, 10)// 两个参数
  sum(1, 2, 4) // 两个参数
</script>

剩余参数

<script>
  function config(baseURL, ...other) {
    console.log(baseURL) // 得到 'http://baidu.com'
    console.log(other)  // other  得到 ['get', 'json']
  }
  // 调用函数
  config('http://baidu.com', 'get', 'json');
</script>

总结:

  1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参
  2. 借助 ... 获取的剩余实参,是个真数组

*箭头函数

箭头函数属于表达式函数,因此不存在函数提升

  1. 基本语法:
(param1, param2, ..., paramN) => { statements }

// 对于只有一个参数的情况,圆括号可以省略:

param => { statements }

// 如果函数体只有一条语句,并且这条语句的返回值就是函数的返回值,那么可以进一步简化,省略花括号:

(param1, param2, ..., paramN) => expression
// 或者
param => expression
  1. 箭头函数中没有 arguments,只能使用 ... 动态获取实参
<body>
  <script>
    // 1. 利用箭头函数来求和
    const getSum = (...arr) => {
      let sum = 0
      for (let i = 0; i < arr.length; i++) {
        sum += arr[i]
      }
      return sum
    }
    const result = getSum(2, 3, 4)
    console.log(result) // 9
  </script>
  1. 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。

        // 箭头函数的this  是上一层作用域的this 指向
            const fn = () => {
                console.log(this)  // window
            }
            fn()
            // 对象方法箭头函数 this
            const obj = {
                uname: 'ldh',
                sayHi: () => {
                    console.log(this)  // this 指向谁? window
                }
            }
            obj.sayHi()
    
            const obj = {
                uname: 'ldh',
                sayHi: function () {
                    console.log(this)  // obj
                    let i = 10
                    const count = () => {
                        console.log(this)  // obj 
                    }
                    count()
                }
            }
            obj.sayHi()
    
  2. 箭头函数不能用作构造函数,也就是说不能使用new关键字调用箭头函数。

3.展开运算符...

数组展开

  1. 合并数组
   const arr1 = [1, 2, 3];
   const arr2 = [4, 5, 6];
   const combined = [...arr1, ...arr2]; // 结果: [1, 2, 3, 4, 5, 6]
  1. 克隆数组
   const originalArray = [1, 2, 3];
   const clonedArray = [...originalArray]; // 浅克隆
  1. 将字符串或类数组对象转换为数组
   const str = 'abc';
   const arrayFromString = [...str]; // 结果: ['a', 'b', 'c']
   
   const nodeList = document.querySelectorAll('div');
   const divs = [...nodeList]; // 将NodeList转换为数组

函数参数传递

function sum(a, b, c) {
  return a + b + c;
}

const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 结果: 6

对象展开

对于对象,展开运算符可以用于复制对象并添加或覆盖属性,或者将多个对象合并成一个新的对象:

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // 复制并添加新属性(浅拷贝)
const mergedObj = { ...obj1, ...obj2, b: 3 }; // 合并对象并覆盖属性

注意事项

  • 展开运算符不会展开对象的原型链上的属性。

4.解构

JavaScript中的解构(Destructuring)是一种表达式,它允许你从数组或者对象中提取数据,将其赋值给变量。这种语法极大地简化了数据处理,使得代码更简洁、可读性更强。解构分为两种主要类型:数组解构和对象解构。

数组解构

let arr = [1, 2, 3];
let [a, b, c] = arr;

console.log(a); // 输出 1
console.log(b); // 输出 2
console.log(c); // 输出 3

可以使用逗号 , 来跳过某些元素。

let arr = [1, 2, 3, 4, 5];
let [first, , third] = arr;

console.log(first); // 输出 1
console.log(third); // 输出 3

尝试解构的值不存在或者为undefined时,可以设置默认值,且只有单元值为 undefined 时默认值才会生效。

let arr = [1, 2];
let [a, b, c = 3] = arr;

console.log(a); // 输出 1
console.log(b); // 输出 2
console.log(c); // 输出 3

对象解构

对象解构允许你将对象的属性值直接赋给同名的变量。

let obj = {name: 'Alice', age: 30};
let {name, age} = obj;

console.log(name); // 输出 'Alice'
console.log(age); // 输出 30

可以解构嵌套的对象,只需要用点操作符或者方括号表示路径。

let user = {info: {name: 'Bob', age: 25}};
let {info: {name}} = user;

console.log(name); // 输出 'Bob'

可以为解构的属性设置默认值,属性不存在或单元值为 undefined 时默认值才会生效,或者使用不同的变量名来接收属性值(即别名)。

let obj = {name: 'Charlie'};
let {name: userName = 'Default', address = 'Unknown'} = obj;

console.log(userName); // 输出 'Charlie'
console.log(address); // 输出 'Unknown'

5.数组方法

forEach遍历数组

用于遍历数组的每个元素,并对每个元素执行提供的函数(回调函数)。这个方法不会创建新数组,也不会改变原数组,**纯粹是为了执行回调函数遍历数组而存在。**它不返回任何值(返回值是 undefined)。

<body>
  <script>
    // forEach 就是遍历  加强版的for循环  适合于遍历数组对象
    const arr = ['red', 'green', 'pink']
    const result = arr.forEach(function (item, index) {
      console.log(item)  // 数组元素 red  green pink
      console.log(index) // 索引号
    })
  </script>
</body>

filter筛选数组

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素

主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组

<body>
  <script>
    const arr = [10, 20, 30]
    // const newArr = arr.filter(function (item, index) {
    //   return item >= 20
    // })
    // 返回的符合条件的新数组
    const newArr = arr.filter(item => item >= 20)
    console.log(newArr)
  </script>
</body>

6.构造函数

构造函数是专门用于创建对象的函数,如果一个函数使用 new 关键字调用,那么这个函数就是构造函数。

<script>
  // 定义函数
  function foo() {
    console.log('通过 new 也能调用函数...');
  }
  // 调用函数
  new foo;
</script>

总结:

  1. 实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。

  2. 使用 new 关键字调用函数的行为被称为实例化

  3. 实例化构造函数时没有参数时可以省略 ()

  4. 构造函数的返回值即为新创建的对象

  5. 构造函数内部的 return 返回的值无效!

实例成员

通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。

<script>
  // 构造函数
  function Person() {
    // 构造函数内部的 this 就是实例对象
    // 实例对象中动态添加属性
    this.name = '小明'
    // 实例对象动态添加方法
    this.sayHi = function () {
      console.log('大家好~')
    }
  }
  // 实例化,p1 是实例对象
  // p1 实际就是 构造函数内部的 this
  const p1 = new Person()
  console.log(p1)
  console.log(p1.name) // 访问实例属性
  p1.sayHi() // 调用实例方法
</script>

总结:

  1. 构造函数内部 this 实际上就是实例对象,为其动态添加的属性和方法即为实例成员
  2. 为构造函数传入参数,动态创建结构相同但值不同的对象

注:构造函数创建的实例对象彼此独立互不影响。

静态成员

在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。

<script>
  // 构造函数
  function Person(name, age) {
    // 省略实例成员
  }
  // 静态属性
  Person.eyes = 2
  Person.arms = 2
  // 静态方法
  Person.walk = function () {
    console.log('^_^人都会走路...')
    // this 指向 Person
    console.log(this.eyes)
  }
</script>

总结:

  1. 静态成员指的是添加到构造函数本身的属性和方法
  2. 一般公共特征的属性或方法静态成员设置为静态成员
  3. 静态成员方法中的 this 指向构造函数本身

原型对象

JavaScript中的每个函数都有一个prototype属性,这个属性是一个对象,用于继承。当构造函数创建新实例时,这些实例会从构造函数的prototype继承属性和方法。这意味着所有实例都可以访问定义在原型上的方法,从而节省内存。

  • 构造函数和原型对象中的this 都指向 实例化的对象
<script>
  function Person() {
    // 此处定义同名方法 sayHi
    this.sayHi = function () {
      console.log('嗨!');
    }
  }

  // 为构造函数的原型对象添加方法
  Person.prototype.sayHi = function () {
    console.log('Hi~');
  }

  let p1 = new Person();
  p1.sayHi(); // 输出结果为 嗨!
</script>

当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。

constructor 属性

每个由构造函数创建的对象都自动拥有一个constructor属性,指向创建它的构造函数。

function Person(name, age) {
    this.name = name;
    this.age = age;

    // 可以在这里定义方法
    this.greet = function() {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    };
}


let person1 = new Person('Alice', 30);
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
let person2 = new Person('Bob', 25);
person2.greet(); // 输出: Hello, my name is Bob and I am 25 years old.


console.log(person1.constructor === Person); // 输出: true


Person.prototype.sayGoodbye = function() {
    console.log(`Goodbye, my name is ${this.name}.`);
};
person1.sayGoodbye(); // 输出: Goodbye, my name is Alice.
person2.sayGoodbye(); // 输出: Goodbye, my name is Bob.

7.面向对象思想

封装性

js面向对象可以通过构造函数实现的封装。

同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的。

<script>
  function Person() {
    this.name = '佚名'
    // 设置名字
    this.setName = function (name) {
      this.name = name
    }
    // 读取名字
    this.getName = () => {
      console.log(this.name)
    }
  }

  // 实例对像,获得了构造函数中封装的所有逻辑
  let p1 = new Person()
  p1.setName('小明')
  console.log(p1.name)

  // 实例对象
  let p2 = new Person()
  console.log(p2.name)
</script>

继承性

继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承

对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。

查看原型: 你可以使用__proto__来查看一个对象的原型。

  const obj = {};
  console.log(obj.__proto__); // 这将输出对象的原型,即Object.prototype

原型链:在JavaScript中,每个对象都有一个原型对象(prototype object),当试图访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript引擎会继续在其原型对象中查找(也就是 __proto__指向的 prototype 原型对象),这个过程会一直向上追溯,直到找到该属性或方法,或者达到原型链的末端(通常是null)。

 <body>
  <script>
    function Person() {
      this.eyes = 2
      this.head = 1
    }
    // console.log(new Person)
    // 女人  构造函数   继承  想要 继承 Person
    function Woman() {

    }
    // Woman 通过原型来继承 Person
    Woman.prototype = new Person()   // {eyes: 2, head: 1} 
    // 指回原来的构造函数
    Woman.prototype.constructor = Woman

    // 给女人添加一个方法  生孩子
    Woman.prototype.baby = function () {
      console.log('宝贝')
    }
    const red = new Woman()
    console.log(red)
    // console.log(Woman.prototype)
    // 男人 构造函数  继承  想要 继承 Person
    function Man() {

    }
    // 通过 原型继承 Person
    Man.prototype = new Person()
    Man.prototype.constructor = Man
    const pink = new Man()
    console.log(pink)
  </script>
</body>

instanceof 运算符

可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

	console.log(Object.prototype)
    console.log(Object.prototype.__proto__) //null

    function Person() {
    }
    const ldh = new Person()
    console.log(ldh instanceof Person)
    console.log(ldh instanceof Object)
    console.log(ldh instanceof Array)
    console.log([1, 2, 3] instanceof Array)
    console.log(Array instanceof Object)

8.改变this的指向

有 3 个方法可以动态指定普通函数中 this 的指向:

call

使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:

<script>
  // 普通函数
  function sayHi() {
    console.log(this);
  }

  let user = {
    name: '小明',
    age: 18
  }

  let student = {
    name: '小红',
    age: 16
  }

  // 调用函数并指定 this 的值
  sayHi.call(user); // this 值为 user
  sayHi.call(student); // this 值为 student

  // 求和函数
  function counter(x, y) {
    return x + y;
  }

  // 调用 counter 函数,并传入参数
  let result = counter.call(null, 5, 10);
  console.log(result);
</script>

总结:

  1. call 方法能够在调用函数的同时指定 this 的值
  2. 使用 call 方法调用函数时,第1个参数为 this 指定的值
  3. call 方法的其余参数会依次自动传入函数做为函数的参数

apply

使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:

<script>
  // 普通函数
  function sayHi() {
    console.log(this)
  }

  let user = {
    name: '小明',
    age: 18
  }

  let student = {
    name: '小红',
    age: 16
  }

  // 调用函数并指定 this 的值
  sayHi.apply(user) // this 值为 user
  sayHi.apply(student) // this 值为 student

  // 求和函数
  function counter(x, y) {
    return x + y
  }
  // 调用 counter 函数,并传入参数
  let result = counter.apply(null, [5, 10])
  console.log(result)
</script>

总结:

  1. apply 方法能够在调用函数的同时指定 this 的值
  2. 使用 apply 方法调用函数时,第1个参数为 this 指定的值
  3. apply 方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数

bind

bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数,使用方法如下代码所示:

<script>
  // 普通函数
  function sayHi() {
    console.log(this)
  }
  let user = {
    name: '小明',
    age: 18
  }
  // 调用 bind 指定 this 的值
  let sayHello = sayHi.bind(user);
  // 调用使用 bind 创建的新函数
  sayHello()
</script>
  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值