JS高级阶段总结

首页 / 技术积累 / 正文

其实JS高级阶段我觉得我应该会总结的很烂,因为JS高级部分确实是很多原理,并且更加底层的东西我应该是还没摸透,但本着加深自己印象的想法,我还是决定写下这篇总结,为了更贴合JS高级的理论,我会对着课堂笔记加上自己的理解去总结,如果有总结的不好的地方欢迎指出。

作用域与闭包的问题

首先,作用域就是为了限制变量的访问范围,防止命名冲突,由此分为全局作用域和局部作用域,当然ES6还新增了块级作用域(归属于局部作用域),区别就是全局作用域允许整个文件的任意位置访问,而局部作用域就是外部无法直接访问的,比如在函数内部声明的变量只能在函数内部被访问。

块级作用域比较特殊,就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域,这样可以解决之前var声明带来的一系列问题,比如本该销毁的变量不会自动销毁,而块级作用域的代码会在代码执行完成之后销毁,并且块级作用域不会变量提升,这就能够使你的代码更加规范。

再讲讲闭包的概念,闭包不是一个新的语法,就是一种特殊的代码形式,体现在代码上是:内部函数访问外部函数的局部变量,在使用上是在外面调用里面的函数,比如

<script>
    function foo() {
        let i = 0;

        // 函数内部分函数
        function bar() {
            console.log(++i);
        }

        // 将函数做为返回值
        return bar;
    }

    // fn 即为闭包函数
    let fn = foo();

    fn();
    fn();
</script>

这就是典型的累加器应用,调用一次实现加1的操作,但这就造成一个问题,即内存泄漏,函数执行完毕后,函数内的局部变量没有释放,占用内存的时间会变长。

一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。

函数进阶

JS代码并不是上来就执行的,在执行之前还有一个解析的阶段,这就是预解析,函数在预解析的时候会提升到代码的最上方,也就是函数无需考虑先后顺序。

动态参数

有些时候用户给函数的传参是不固定的,所以可以使用arguments获取用户所有传递的实参,arguments是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参,伪数组即有length属性的对象,看起来是数组,但实际是没有数组的方法的。

上述方法还不够灵活,因为arguments是伪数组,使用起来不是很方便,所以引出了剩余参数的概念,借助展开运算符即可实现,展开运算符就是将数组的每一项以逗号分隔展开,实际应用很广。

<script>
  function sum() {
    // console.log(arguments)
    let s = 0
    for(let i = 0; i < arguments.length; i++) {
      s += arguments[i]
    }
    console.log(s)
  }
  function config(baseURL, ...other) {
    console.log(baseURL) // 得到 'http://baidu.com'
    console.log(other)  // other  得到 ['get', 'json']
  }
  // 调用函数
  config('http://baidu.com', 'get', 'json');
</script>

箭头函数

箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,区别就在于this的指向和arguments的问题了,this在普通函数下是谁调用就指向谁,而箭头函数内部是没有this的,它的this是继承而来; 默认指向在定义它时所处的宿主对象,而不是执行时的对象, 定义它的时候,可能环境是window,这就很适合运用在setTimeout之类的异步操作里,因为异步操作会运行在与所在函数完全分离的执行环境上;

解构赋值

解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型,其中对象用的比较多。

数组解构

<script>
  // 普通的数组
  let arr = [1, 2, 3];
  // 批量声明变量 a b c 
  // 同时将数组单元值 1 2 3 依次赋值给变量 a b c
  let [a, b, c] = arr;
  console.log(a); // 1
  console.log(b); // 2
  console.log(c); // 3
</script>

对象解构

<script>
  // 普通对象
  const user = {
    name: '小明',
    age: 18
  };
  // 批量声明变量 name age
  // 同时将数组单元值 小明  18 依次赋值给变量 name  age
  const {name, age} = user

  console.log(name) // 小明
  console.log(age) // 18
</script>

同时可以使用:进行改名,这一点用在函数传参的时候解构比较多。

构造函数

构造函数写法和普通函数写法一致,为了区别,有一个约定俗成的写法,那就是构造函数的名字首字母一般是大写,同时需要配合new关键字使用。

大概就是用于批量生产实例对象,使每个实例对象都有默认的属性和方法,其中JS内置了很多构造函数,几乎为每一种数据类型都提供了对应的构造函数,除undefined与null外。

<script>
/* 
数据类型                 内置构造函数
number                  Number()
string                  String()
boolean                 Boolean()

undefined               无
null                    无

array                   Array()
object                  Object()
function                Function()
*/

// 只要是构造函数就可以使用new来调用
let n = new Number(10);     // let n = 10;
console.log(n);
</script>

因为有了这些内置的构造函数和原型链,我们就可以使用很多内置的方法了,比如数组的map,对象的keys等等等等,其中还有一个小知识点,那就是基本包装数据,可以理解为也是JS中的一种数据类型的隐式转换,只不过这种转换是将数据转换为对象。

比如一个不是对象的变量使用了点语法赋值一个属性,JS不会报错,而是创建了一个临时对象,在这个临时对象上操作后再将其删除,所以如果访问这个属性得到的是undefined。

原型

这一章节挺难的,我表示已经晕了。

在JS中,只要有一个函数存在,JS就会创建一个与之对应的对象,这个对象就是原型对象。这个对象只有一个属性 constructor 指向这个函数,用处就是为了继承,就是当一个实例对象访问某个成员时,如果不存在会自动到原型对象上找。

<script>
  function Person(){}
// 在原型对象上添加成员
Person.prototype.age = 22
Person.prototype.say = function(){
 console.log('hello');
}

// 得到实例对象
let obj = new Person();

console.log(obj);
console.log(obj.__proto__);

// 实例对象访问某个成员不存在是会到原型对象上找
console.log(obj.age);
obj.say();

// 验证:同一个构造函数实例化的对象,共享同一个成员。

let obj2 = new Person();
console.log(obj.say === obj2.say)
</script>

构造函数、原型对象、实例对象的三角关系

1.png

  • 构造函数.prototype 指向原型对象.
  • 原型对象.constructor 指向构造函数.
  • 实例对象._proto_ 指向原型对象.

原型继承与原型链

原型继承就是将父的原型对象替换子的原型对象,这样子就能够使用父上的属性和方法了。

<script>
    // Human为父
  function Human() {
      this.eyes = 2;
      this.legs = 2;
      this.say = function () {
          console.log("hello world");
      };
  }
  
  function Woman() { }
  function Man() { }
  
  // 1. 得到父的实例
  let ha = new Human();
  let hb = new Human();
  
  // 2. 使用父的实例替换子的元素
  Woman.prototype = ha;
  let wa = new Woman();
  console.log(wa.eyes);
  
  // 2. 使用父的实例替换子的元素
  Man.prototype = hb;
  let ma = new Man();
  console.log(ma.legs);
  
  ma.say();
</script>

原型链是由原型对象形成的一个链条,是通过__proto__形成的链接,当原型对象上访问某个成员不存在时,会一级一级往上找,采用就近原则,原型链的终点是null。

2.png

最后还有一个instanceof运算符,可以判断构造函数对应的原型对象是否在以实例为起点的原型链上,这么说可能有点懵,看下方代码。

<script>
  function Human(){}
  function Person(){}
  
  let oa = new Person();
  console.log(oa instanceof Person);      // true
  console.log(oa instanceof Human);       // false
  console.log(oa instanceof Object);       // true
</script>

深浅拷贝

之前WEBAPI的时候总结过简单数据类型和复杂数据类型了,简单数据类型没有深拷贝的说法,直接赋值就是一份全新的,但如果是复杂数据类型进行浅拷贝,就导致一个问题,那就是如果改变了拷贝的那份数据,那么原数据也会跟着改变,这是不合理的,为此我们需要进行深度拷贝,以下归纳两种原生的解决方式,个人感觉很适合锻炼思维,当然第三方的库也有很多。

递归实现

<script>
  let obj = {
      age: 10,
      b: [0, 1, 2],
      c: {
          age: 20,
          b: [3, 4, 5]
      }
  };
  function deepCopy(t, o) {
      for (let key in o) {
        /* 如果遍历的每一位是数组或对象那么重新调用 */
          if (Array.isArray(o[key])) {
              t[key] = [];
              deepCopy(t[key], o[key]);
          } else if (o[key] instanceof Object) {
              t[key] = {};
              deepCopy(t[key], o[key]);
          } else {
              t[key] = o[key];
          }
      }
  }
  let target = {};
  deepCopy(target, obj);
  target.age = 20;
  console.log(obj, target);
</script>

如果遍历的每一位是数组或对象那么重新调用,直到都是简单数据类型为止,注意递归一定要有结束的条件,否则会报浏览器达到最大调用栈的错误。

JSON 方法实现

使用JSON.stringify(对象或数组)将对象转成字符串,再JSON.parse(JSON字符串)转成对象,这样就能够实现深拷贝了,JSON.parse会返回一个全新的对象,相比上面的写法,这个更加简单,第三方库有很多这里就不介绍了。

改变this指向

<script>
let oa = {
      age: 20,
      say: function (a, b, c) {
          console.log(this.age);
          console.log(a, b, c);
      }
  };
  let ob = {
      age: 22
  };
  let oc = {
      age: 25
  };

  oa.say.call(ob, 10, 20, 30);
  oa.say.apply(oc, ['hello', 'world', 'abc'])
  let fn = oa.say.bind(ob, 10, 20, 30);
  fn()
  
  /* 区别分别是
  call             接收参数是独立传递
  apply            接收参数是数组

  call与apply除了接受的参数没有区别,并且会立即调用前面的方法
  bind 会替换this的指向但不会立即调用前面的方法,而是返回一个函数等待你调用
  */
</script>

节流与防抖

节流是在一个时间间隔内,多次触发,事件处理函数仅执行1次。

防抖是在多个重叠的时间间隔内触的事件,只以最后一次触发为准。

两者很容易搞混,但只要知道应用就好办了,一般节流运用在高频触发的事件中,比如页面滚动,窗口大小,一段时间内只允许触发一次,而防抖主要运用在搜索框中,当检测用户停止输入一段时间后发起一个相关的搜索建议请求,而如果用户一直在输入则重新计时。

最后

我竟然真的总结完了,JS高级讲的东西太多了,而且还全是理论,还好是学过一点点,接受起来比较轻松,虽然说总结的时候还是遇到了忘记的知识点,但还好是跟着笔记又复习了一遍,把不懂的地方又疯狂百度了一番,太难啦,因为国庆节想整点欢快点的文章,不搞那么严肃哈哈,所以现在就总结了,最后,祝大家国庆节快乐鸦~

评论区
头像
    头像
    青灯暮雨
    2022年09月29日 09:10
    回复

    不错,学习一下