JavaScript学习笔记

47 阅读38分钟

一、基本语法

1.1、变量

  • 规则:

    • 不能用关键字(varletconstforif);

    • 关键字:有特殊含义的字符,javascript内置的一些英语词汇,如:letvariffor等;

    • 只能用下划线、字母、数字、$组成,且不能以数字开头;

    • 字母严格区分大小写,如Ageage是不同的变量。

  • 规范

    • 起名要有意义;

    • 遵守小驼峰命名法。

1.2、关键字

  • var

    • 在ES5中,顶层对象的属性和全局变量是等价的,用 var 声明的变量既是全局变量,也是顶层变量;

      注意:顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。

    • 使用 var 声明的变量存在变量提升的情况;

    • 使用var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明;

    • 在函数中使用使用 var 声明变量时候,该变量是局部的,而如果在函数内不使用 var ,该变量是全局的。

  • let

    • 用法类似于 var ,但是所声明的变量,只在 let 命令所在的代码块内有效;

    • 不存在变量提升,只要块级作用域内存在 let 命令,这个区域就不再受外部影响;

    • 使用 let 声明变量前,该变量都不可用,也就是大家常说的”暂时性死区“;

    • let不允许在相同作用域中重复声明。

  • const

    • const 声明一个只读的常量,一旦声明,常量的值就不能改变,这意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值;

    • const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动;

    • 对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量;

    • 对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的,并不能确保改变量的结构不变。

  • 三者区别

    • 变量提升;

    • 暂时性死区;

    • 重复声明;

    • 修改声明的变量;

    • 块级作用域(作用域链,经典for循环,闭包)。

1.3、数据类型

1、基本数据类型

  • Number

    • 数值最常见的整数类型格式则为十进制,还可以设置八进制(零开头)、十六进制(0x 开头);

    • 浮点类型则在数值汇总必须包含小数点,还可通过科学计数法表示;

    • 在数值类型中,存在一个特殊数值 NaN ,意为“不是数值”,用于表示本来要返回数值的操作失败了(而不是抛出错误)。

  • String

    • 字符串可以使用双引号(")、单引号(')或反引号(`)标示;

    • 字符串是不可变的,意思是一旦创建,它们的值就不能变了。

  • Boolean

    数据类型转换为 true 的值转换为 false 的值
    String非空字符串“”
    Number非零数值(包括无穷值)0、NaN
    Object任意对象null
    UndefinedN/A(不存在)undefined
  • Undefined

    • Undefined 类型只有一个值,就是特殊值 undefined。当使用 varlet 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值;

    • 包含 undefined 值的变量跟未声明变量是有区别的,未声明的变量会报错。

  • Null

    • Null 类型同样只有一个值,即特殊值 null

    • 逻辑上讲, null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因;

    • undefined 值是由 null 值派生而来,console.log(null == undefined); // true

  • Symbol

    • Symbol 是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

2、复杂数据类型

  • Object

  • Function

  • Array

  • Date

  • RegExp

  • Map

  • Set

3、区别

  • 声明变量时不同的内存地址分配

    • 简单类型的值存放在栈中,在栈中存放的是对应的值;

    • 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址。

  • 不同的类型数据导致赋值变量时的不同

    • 简单类型赋值,是生成相同的值,两个对象对应不同的地址;

    • 复杂类型赋值,是将保存对象的内存地址赋值给另一个变量,也就是两个变量指向堆内存中同一个对象。

1.4、深浅拷贝

1、浅拷贝

  • 浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝;

    • 如果属性是基本类型,拷贝的就是基本类型的值;

    • 如果属性是引用类型,拷贝的就是内存地址。

    即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。

  • Object.assign(target, source1, source2, ...)

    const target = { a: 1, b: 2 };
    const source = { b: 4, c: 5 };
    
    const returnedTarget = Object.assign(target, source);
    
    console.log(target); // { a: 1, b: 4, c: 5 }
    
    console.log(returnedTarget === target); // true
    
  • Array.prototype.slice()

    const fxArr = ["One", "Two", "Three"]
    const fxArrs = fxArr.slice(0)
    fxArrs[1] = "love";
    console.log(fxArr) // ["One", "Two", "Three"]
    console.log(fxArrs) // ["One", "love", "Three"]
    
  • Array.prototype.concat()

    const fxArr = ["One", "Two", "Three"]
    const fxArrs = fxArr.concat()
    fxArrs[1] = "love";
    console.log(fxArr) // ["One", "Two", "Three"]
    console.log(fxArrs) // ["One", "love", "Three"]
    
  • 使用拓展运算符实现的复制

    const fxArr = ["One", "Two", "Three"]
    const fxArrs = [...fxArr]
    fxArrs[1] = "love";
    console.log(fxArr) // ["One", "Two", "Three"]
    console.log(fxArrs) // ["One", "love", "Three"]
    

2、深拷贝

  • 深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

  • _.cloneDeep()

    const _ = require('lodash');
    const obj1 = {
        a: 1,
        b: { f: { g: 1 } },
        c: [1, 2, 3]
    };
    const obj2 = _.cloneDeep(obj1);
    console.log(obj1.b.f === obj2.b.f); // false
    
  • jQuery.extend()

    const $ = require('jquery');
    const obj1 = {
        a: 1,
        b: { f: { g: 1 } },
        c: [1, 2, 3]
    };
    const obj2 = $.extend(true, {}, obj1);
    console.log(obj1.b.f === obj2.b.f); // false
    
  • JSON.stringify()

    const obj2 = JSON.parse(JSON.stringify(obj1));
    

    但是这种方式存在弊端,会忽略 undefinedsymbol 和函数

    const obj = {
        name: 'A',
        name1: undefined,
        name3: function() {},
        name4:  Symbol('A')
    }
    const obj2 = JSON.parse(JSON.stringify(obj));
    console.log(obj2); // {name: "A"}
    
  • 手写循环递归

    function deepClone(obj, hash = new WeakMap()) {
      if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
      if (obj instanceof Date) return new Date(obj);
      if (obj instanceof RegExp) return new RegExp(obj);
      // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
      if (typeof obj !== "object") return obj;
      // 是对象的话就要进行深拷贝
      if (hash.get(obj)) return hash.get(obj);
      let cloneObj = new obj.constructor();
      // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
      hash.set(obj, cloneObj);
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          // 实现一个递归拷贝
          cloneObj[key] = deepClone(obj[key], hash);
        }
      }
      return cloneObj;
    }
    

3、区别

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址;

  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址。

1.5、类型转换机制

1、显示转换

  • Number()

    • Number() 转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN

      原始值转换结果
      undefinedNaN
      null0
      true1
      false0
      String根据语法和转换规则来转换
      Symbol抛出类型错误异常
      Object先调用toPrimitive,再调用toNumber
  • parseInt()

    • parseInt()相比Number(),就没那么严格了,parseInt()函数逐个解析字符,遇到不能转换的字符就停下来。

  • String()

    • 可以将任意类型的值转化成字符串

      原始值转换结果
      undefined“undefined”
      Boolean“true”或“false”
      Number对应数字的字符串
      StringString
      Symbol抛出类型错误异常
      Object先调用toPrimitive,再调用toNumber
  • Boolean()

    • 可以将任意类型的值转为布尔值,转换规则如下

      数据类型转换为true的值转换为false的值
      Booleantruefalse
      String非空字符串“”(空字符串)
      Number非零数值(包括无穷值)0、NaN
      Object任意对象null
      undefinedN/A(不存在)undefined

2、隐式转换

我们这里可以归纳为两种情况发生隐式转换的场景

  • 比较运算( ==!=>< )、ifwhile 需要布尔值地方;

  • 算术运算(+-*/%)。

1、自动转换为布尔值

  • 在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean函数;

  • 可以得出个小结:

    • undefined

    • null

    • false

    • +0

    • -0

    • NaN

    • ""

  • 除了上面几种会被转化成 false ,其他都换被转化成 true

2、自动转换成字符串

  • 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串;

  • 具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串;

  • 常发生在 + 运算中,一旦存在字符串,则会进行字符串拼接操作。

    '5' + 1 // '51'
    '5' + true // "5true"
    '5' + false // "5false"
    '5' + {} // "5[object Object]"
    '5' + [] // "5"
    '5' + function (){} // "5function (){}"
    '5' + undefined // "5undefined"
    '5' + null // "5null"
    

3、自动转换为数值

  • 除了 + 有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值;

    '5' - '2' // 3
    '5' * '2' // 10
    true - 1  // 0
    false - 1 // -1
    '1' - 1   // 0
    '5' * []    // 0
    false / '5' // 0
    'abc' - 1   // NaN
    null + 1 // 1
    undefined + 1 // NaN
    
  • null 转为数值时,值为 0undefined 转为数值时,值为 NaN

3、==和===

1、==

  • 两个都为简单类型,字符串和布尔值都会转换成数值,再比较;

  • 简单类型与引用类型比较,对象转化成其原始类型的值,再比较;

  • 两个都为引用类型,则比较它们是否指向同一个对象;

  • nullundefined 相等;

  • 存在 NaN 则返回 false

2、===

  • 全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 true,即类型相同,值也需相同;

  • undefinednull 与自身严格相等。

二、字符串

2.1、操作方法

1、增

  • 除了常用 + 以及 ${} 进行字符串拼接之外,还可通过 concat

  • concat

    用于将一个或者多个字符串拼接成一个新的字符串

    let stringValue = "hello ";
    let result = stringValue.concat("world");
    console.log(result); // "hello world"
    console.log(stringValue); // "hello"
    

2、删

  • slice()

  • substr()

  • substring()

    这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。

    let stringValue = "hello world";
    console.log(stringValue.slice(3)); // "lo world"
    console.log(stringValue.substring(3)); // "lo world"
    console.log(stringValue.substr(3)); // "lo world"
    console.log(stringValue.slice(3, 7)); // "lo w"
    console.log(stringValue.substring(3,7)); // "lo w"
    console.log(stringValue.substr(3, 7)); // "lo worl"
    

3、改

  • trim()trimLeft()trimRight()

    • 删除前、后或前后所有空格符,再返回新的字符串;

      let stringValue = " hello world ";
      let trimmedStringValue = stringValue.trim();
      console.log(stringValue); // " hello world "
      console.log(trimmedStringValue); // "hello world"
      
  • repeat()

    • 接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果;

      let stringValue = "na ";
      let copyResult = stringValue.repeat(2) // na na 
      
  • padStart()padEnd()

    • 复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件;

      let stringValue = "foo";
      console.log(stringValue.padStart(6)); // " foo"
      console.log(stringValue.padStart(9, ".")); // "......foo"
      
  • toLowerCase()toUpperCase()

    • 大小写转化;

      let stringValue = "hello world";
      console.log(stringValue.toUpperCase()); // "HELLO WORLD"
      console.log(stringValue.toLowerCase()); // "hello world"
      

4、查

  • chatAt()

    • 返回给定索引位置的字符,由传给方法的整数参数指定;

      let message = "abcde";
      console.log(message.charAt(2)); // "c"
      
  • indexOf()

    • 从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 );

      let stringValue = "hello world";
      console.log(stringValue.indexOf("o")); // 4
      
  • startWith()includes()

    • 从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值;

      let message = "foobarbaz";
      console.log(message.startsWith("foo")); // true
      console.log(message.startsWith("bar")); // false
      console.log(message.includes("bar")); // true
      console.log(message.includes("qux")); // false
      

2.2、转换方法

  • split

    把字符串按照指定的分割符,拆分成数组中的每一项;

    let str = "12+23+34"
    let arr = str.split("+") // [12,23,34]
    

2.3、模板匹配方法

针对正则表达式,字符串设计了几个方法:

  • match()

    • 接收一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象,返回数组;

      let text = "cat, bat, sat, fat";
      let pattern = /.at/;
      let matches = text.match(pattern);
      console.log(matches[0]); // "cat"
      
  • search()

    • 接收一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象,找到则返回匹配索引,否则返回 -1;

      let text = "cat, bat, sat, fat";
      let pos = text.search(/at/);
      console.log(pos); // 1
      
  • replace()

    • 接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数);

      let text = "cat, bat, sat, fat";
      let result = text.replace("at", "ond");
      console.log(result); // "cond, bat, sat, fat"
      

2.4、正则表达式

1、创建

  • 字面量创建,其由包含在斜杠之间的模式组成;

  • 调用 RegExp 对象的构造函数;

    const re = /\d+/g;
    
    const re = new RegExp("\\d+", "g");
    const rul = "\\d+"
    const re1 = new RegExp(rul, "g");
    
  • 使用构建函数创建,第一个参数可以是一个变量,遇到特殊字符 \ 需要使用 \\ 进行转义。

2、匹配规则

规则描述
\转义
匹配输入的开始
$匹配输入的结束
*匹配前一个表达式 0 次或多次
.默认匹配除换行符之外的任何单个字符
\b匹配一个词的边界,例如在字母和空格之间
\B匹配一个非单词边界
\d匹配一个数字
\D匹配一个非数字字符
\n匹配一个换行符
\r匹配一个回车符
\s匹配一个空白字符,包括空格、制表符、换页符和换行符
\S匹配一个非空白字符
\w匹配一个单字字符(字母、数字或者下划线)
\W匹配一个非单字字符

3、匹配方法

方法描述
exec一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。
test一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。
match一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。
matchAll一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。
search一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。
replace一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。
split一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的String方法。

三、数组

3.1、操作方法

1、增

  • push()

    • push() 方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。

      let colors = []; // 创建一个数组
      let count = colors.push("red", "green"); // 推入两项
      console.log(count) // 2
      
  • unshift()

    • unshift()在数组开头添加任意多个值,然后返回新的数组长度。

      let colors = new Array(); // 创建一个数组
      let count = colors.unshift("red", "green"); // 从数组开头推入两项
      alert(count); // 2
      
  • splice()

    • 传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组。

      let colors = ["red", "green", "blue"];
      let removed = colors.splice(1, 0, "yellow", "orange")
      console.log(colors) // red,yellow,orange,green,blue
      console.log(removed) // []
      
  • concat()

    • 首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组。

      let colors = ["red", "green", "blue"];
      let colors2 = colors.concat("yellow", ["black", "brown"]);
      console.log(colors); // ["red", "green","blue"]
      console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
      

2、删

  • pop()

    • pop() 方法用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项。

      let colors = ["red", "green"]
      let item = colors.pop(); // 取得最后一项
      console.log(item) // green
      console.log(colors.length) // 1
      
  • shift()

    • shift()方法用于删除数组的第一项,同时减少数组的length 值,返回被删除的项。

      let colors = ["red", "green"]
      let item = colors.shift(); // 取得第一项
      console.log(item) // red
      console.log(colors.length) // 1
      
  • splice()

    • 传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组。

      let colors = ["red", "green", "blue"];
      let removed = colors.splice(0,1); // 删除第一项
      console.log(colors); // green,blue
      console.log(removed); // red,只有一个元素的数组
      
  • slice()

    • slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组。

      let colors = ["red", "green", "blue", "yellow", "purple"];
      let colors2 = colors.slice(1);
      let colors3 = colors.slice(1, 4);
      console.log(colors)   // red,green,blue,yellow,purple
      concole.log(colors2); // green,blue,yellow,purple
      concole.log(colors3); // green,blue,yellow
      

3、改

  • splice()

    • 传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响。

      let colors = ["red", "green", "blue"];
      let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
      console.log(colors); // red,red,purple,blue
      console.log(removed); // green,只有一个元素的数组
      

4、查

  • indexOf()

    • 返回要查找的元素在数组中的位置,如果没找到则返回 -1。

      let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
      numbers.indexOf(4) // 3
      
  • includes()

    • 返回要查找的元素在数组中的位置,找到返回true,否则false

      let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
      numbers.includes(4) // true
      
  • find()

    • 返回第一个匹配的元素。

      const people = [
          {
              name: "Matt",
              age: 27
          },
          {
              name: "Nicholas",
              age: 29
          }
      ];
      people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}
      

3.2、ES6新增

1、扩展运算符

  • 可以将数组转换为用逗号分隔的参数序列,主要用于函数调用的时候,将一个数组变为参数序列;

    const arr = [1, 2, 3];
    fn(...arr)
    
  • 可以将某些数据结构转为数组;

    [...document.querySelectorAll('div')]
    
  • 能够更简单实现数组复制;

    const a1 = [1, 2];
    const [...a2] = a1;
    
  • 数组快速合并;

    const arr1 = ['a', 'b'];
    const arr2 = ['c'];
    const arr3 = ['d', 'e'];
    [...arr1, ...arr2, ...arr3]
    // [ 'a', 'b', 'c', 'd', 'e' ]
    

    通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组。

  • 扩展运算符可以与解构赋值结合起来,用于生成数组;

    const [first, ...rest] = [1, 2, 3, 4, 5];
    first // 1
    rest  // [2, 3, 4, 5]
    
    const [first, ...rest] = [];
    first // undefined
    rest  // []
    
    const [first, ...rest] = ["foo"];
    first  // "foo"
    rest   // []
    
  • 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错;

    const [...butLast, last] = [1, 2, 3, 4, 5];
    // 报错
    
    const [first, ...middle, last] = [1, 2, 3, 4, 5];
    // 报错
    
  • 可以将字符串转为真正的数组;

    [...'hello']
    // [ "h", "e", "l", "l", "o" ]
    
  • 定义了遍历器接口的对象,都可以用扩展运算符转为真正的数组,如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。

    const obj = {a: 1, b: 2};
    let arr = [...obj]; // TypeError: Cannot spread non-iterable object
    

2、构造函数新增方法

  • Array.from()

    • 将两类对象转为真正的数组:类似数组的对象和可遍历的对象(包括 ES6 新增的数据结构 SetMap);

      let arrayLike = {
          '0': 'a',
          '1': 'b',
          '2': 'c',
          length: 3
      };
      let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
      
    • 还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组。

      Array.from([1, 2, 3], (x) => x * x)
      // [1, 4, 9]
      
  • Array.of()

    • 用于将一组值,转换为数组;

      Array.of(3, 11, 8) // [3,11,8]
      
    • 没有参数的时候,返回一个空数组;

      Array() // []
      
    • 当参数只有一个的时候,实际上是指定数组的长度;

      Array(3) // [, , ,]
      
    • 参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。

      Array(3, 11, 8) // [3, 11, 8]
      

3、实例对象新增方法

  • copyWithin()

    • 将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组;

    • 参数如下:

      • target(必需):从该位置开始替换数据。如果为负值,表示倒数;

      • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算;

      • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。

        // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
        [1, 2, 3, 4, 5].copyWithin(0, 3);
        // [4, 5, 3, 4, 5]
        
  • find()findIndex()

    • find() 用于找出第一个符合条件的数组成员,参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组;

      [1, 5, 10, 15].find(function(value, index, arr) {
        return value > 9;
      }) // 10
      
    • findIndex() 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

      [1, 5, 10, 15].findIndex(function(value, index, arr) {
        return value > 9;
      }) // 2
      
    • 这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

      function f(v){
        return v > this.age;
      }
      let person = {name: 'John', age: 20};
      [10, 12, 26, 15].find(f, person);    // 26
      
  • fill()

    • 使用给定值,填充一个数组,还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置

      ['a', 'b', 'c'].fill(7)
      // [7, 7, 7]
      
      new Array(3).fill(7)
      // [7, 7, 7]
      
      ['a', 'b', 'c'].fill(7, 1, 2)
      // ['a', 7, 'c']
      

      如果填充的类型为对象,则是浅拷贝

  • entries()keys()values()

    • keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历;

      for (let index of ['a', 'b'].keys()) {
        console.log(index);
      }
      // 0
      // 1
      
      for (let elem of ['a', 'b'].values()) {
        console.log(elem);
      }
      // 'a'
      // 'b'
      
      for (let [index, elem] of ['a', 'b'].entries()) {
        console.log(index, elem);
      }
      // 0 "a"
      
  • includes()

    • 用于判断数组是否包含给定的值;

      [1, 2, 3].includes(2)     // true
      [1, 2, 3].includes(4)     // false
      [1, 2, NaN].includes(NaN) // true
      
    • 方法的第二个参数表示搜索的起始位置,默认为0,参数为负数则表示倒数的位置。

      [1, 2, 3].includes(3, 3);  // false
      [1, 2, 3].includes(3, -1); // true
      
  • flat()flatMap()

    • 将数组扁平化处理,返回一个新数组,对原数据没有影响;

      [1, 2, [3, 4]].flat()
      // [1, 2, 3, 4]
      
    • flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为1;

      [1, 2, [3, [4, 5]]].flat()
      // [1, 2, 3, [4, 5]]
      
      [1, 2, [3, [4, 5]]].flat(2)
      // [1, 2, 3, 4, 5]
      
    • flatMap() 方法对原数组的每个成员执行一个函数相当于执行 Array.prototype.map(),然后对返回值组成的数组执行flat() 方法。该方法返回一个新数组,不改变原数组

      // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
      [2, 3, 4].flatMap((x) => [x, x * 2])
      // [2, 4, 3, 6, 4, 8]
      

      flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

  • sort()

    • 接受一个函数,函数返回值>1,<1,=0(升序)

      [1, 5, 7, 3, 4].sort((a, b) => a - b ) // [1, 3, 4, 5, 7]
      

四、函数

4.1、ES6新增

1、参数

  • ES6允许为函数的参数设置默认值;

    function log(x, y = 'World') {
      console.log(x, y);
    }
    
    console.log('Hello') // Hello World
    console.log('Hello', 'China') // Hello China
    console.log('Hello', '') // Hello
    
  • 函数的形参是默认声明的,不能使用letconst再次声明;

    function foo(x = 5) {
        let x = 1; // error
        const x = 2; // error
    }
    
  • 参数默认值可以与解构赋值的默认值结合起来使用;

    function foo({x, y = 5}) {
      console.log(x, y);
    }
    
    foo({}) // undefined 5
    foo({x: 1}) // 1 5
    foo({x: 1, y: 2}) // 1 2
    foo() // TypeError: Cannot read property 'x' of undefined
    

    上面的foo函数,当参数为对象的时候才能进行解构,如果没有提供参数的时候,变量xy就不会生成,从而报错,这里设置默认值避免。

    function foo({x, y = 5} = {}) {
      console.log(x, y);
    }
    
    foo() // undefined 5
    
  • 参数默认值应该是函数的尾参数,如果不是非尾部的参数设置默认值,实际上这个参数是没法省略的。

    function f(x = 1, y) {
      return [x, y];
    }
    
    f() // [1, undefined]
    f(2) // [2, undefined]
    f(, 1) // 报错
    f(undefined, 1) // [1, 1]
    

2、属性

  • 函数的 length 属性;

    • length 将返回没有指定默认值的参数个数,rest 参数也不会计入 length 属性;

      (function (a) {}).length // 1
      (function (a = 5) {}).length // 0
      (function (a, b, c = 5) {}).length // 2
      (function(...args) {}).length // 0
      
    • 如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数了。

      (function (a = 0, b, c) {}).length // 0
      (function (a, b = 1, c) {}).length // 1
      
  • name 属性。

    • 返回该函数的函数名;

      var f = function () {};
      
      // ES5
      f.name // ""
      
      // ES6
      f.name // "f"
      
    • 如果将一个具名函数赋值给一个变量,则 name 属性都返回这个具名函数原本的名字;

      const bar = function baz() {};
      bar.name // "baz"
      
    • Function 构造函数返回的函数实例,name 属性的值为anonymous

      (new Function).name // "anonymous"
      
    • bind 返回的函数,name 属性值会加上 bound 前缀。

      function foo() {};
      foo.bind({}).name // "bound foo"
      
      (function(){}).bind({}).name // "bound "
      

3、作用域

  • 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域,等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

    下面例子中,y=x 会形成一个单独作用域,x 没有被定义,所以指向全局变量 x

    let x = 1;
    
    function f(y = x) { 
      // 等同于 let y = x  
      let x = 2; 
      console.log(y);
    }
    
    f() // 1
    

4、严格模式

  • 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

    // 报错
    function doSomething(a, b = a) {
      'use strict';
      // code
    }
    
    // 报错
    const doSomething = function ({a, b}) {
      'use strict';
      // code
    };
    
    // 报错
    const doSomething = (...a) => {
      'use strict';
      // code
    };
    
    const obj = {
      // 报错
      doSomething({a, b}) {
        'use strict';
        // code
      }
    };
    

5、箭头函数

  • 使用“箭头”(=>)定义函数;

    var f = v => v;
    
    // 等同于
    var f = function (v) {
      return v;
    };
    
  • 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分;

    var f = () => 5;
    // 等同于
    var f = function () { return 5 };
    
    var sum = (num1, num2) => num1 + num2;
    // 等同于
    var sum = function(num1, num2) {
      return num1 + num2;
    };
    
  • 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回;

    var sum = (num1, num2) => { return num1 + num2; }
    

    如果返回对象,需要加括号将对象包裹

    let getTempItem = id => ({ id: id, name: "Temp" });
    

1、函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;

2、不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误;

3、不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替;

4、不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

4.2、闭包

闭包、函数柯里化、节流防抖 - 掘金 (juejin.cn)

4.3、函数式编程

1、函数式编程概念

  • 函数式编程是一种编程范式、一种编写程序的方法论;

  • 主要的编程范式有三种:命令式编程,声明式编程和函数式编程;

  • 相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程;

  • 简单来讲,就是要把过程逻辑写成函数,定义好输入参数,只关心它的输出结果,即是一种描述集合和集合之间的转换关系,输入通过函数都会返回有且只有一个输出值;

  • 可以看到,函数实际上是一个关系,或者说是一种映射,而这种映射关系是可以组合的,一旦我们知道一个函数的输出类型可以匹配另一个函数的输入,那他们就可以进行组合。

2、实现

  • 纯函数

    • 纯函数是对给定的输入返还相同输出的函数,并且要求你所有的数据都是不可变的,即纯函数=无状态+数据不可变

    • 特性:

      • 函数内部传入指定的值,就会返回确定唯一的值;

      • 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数。

    • 优势

      • 使用纯函数,我们可以产生可测试的代码;

      • 不依赖外部环境计算,不会产生副作用,提高函数的复用性;

      • 可读性更强 ,函数不管是否是纯函数都会有一个语义化的名称,更便于阅读;

      • 可以组装成复杂任务的可能性,符合模块化概念及单一职责原则。

  • 高阶函数

    • 高级函数,就是以函数作为输入或者输出的函数被称为高阶函数;

      通过高阶函数抽象过程,注重结果;

      const forEach = function(arr,fn) {
          for(let i=0; i<arr.length; i++) {
              fn(arr[i]);
          }
      }
      let arr = [1,2,3];
      forEach(arr, (item) => {
          console.log(item);
      })
      

      上面通过高阶函数 forEach 来抽象循环如何做的逻辑,直接关注做了什么。

    • 高阶函数存在缓存的特性,主要是利用闭包作用。

      const once = (fn) => {
          let done = false;
          return function() {
              if(!done) {
                  fn.apply(this, fn);
              } else {
                  console.log("该函数已经执行");
              }
              done = true;
          }
      }
      
  • 柯里化

    • 柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程;

      一个二元函数如下:

      let fn = (x,y) => x + y;
      

      转化成柯里化函数如下:

      const curry = function(fn){
          return function(x){
              return function(y){
                  return fn(x,y);
              }
          }
      }
      let myfn = curry(fn);
      console.log( myfn(1)(2) );
      
    • 上面的 curry 函数只能处理二元情况,下面再来实现一个实现多参数的情况;

      // 多参数柯里化;
      const curry = function(fn) {
          return function curriedFn(...args) {
              if(args.length < fn.length) {
                  return function() {
                      return curriedFn(...args.concat([...arguments]));
                  }
              }
              return fn(...args);
          }
      }
      const fn = (x, y, z, a) => x + y + z + a;
      const myfn = curry(fn);
      console.log(myfn(1)(2)(3)(1));
      
    • 关于柯里化函数的意义如下:

      • 让纯函数更纯,每次接受一个参数,松散解耦;

      • 惰性执行。

  • 组合与管道

    • 组合函数,目的是将多个函数组合成一个函数;

      function afn(a) {
          return a * 2;
      }
      function bfn(b) {
          return b * 3;
      }
      const compose = (a, b) => c => a(b(c));
      let myfn = compose(afn, bfn);
      console.log( myfn(2));
      

      可以看到 compose 实现一个简单的功能:形成了一个新的函数,而这个函数就是一条从 bfn -> afn 的流水线;

    • 管道函数,目的实现一个多函数组合;

      const compose = (...fns) => {
          return val => {
              return fns.reverse().reduce((acc, fn) => {
      			return fn(acc);
          	}, val);
          }
      }
      

      compose 执行是从右到左的。而管道函数,执行顺序是从左到右执行的;

      const pipe = (...fns) => {
          return val => {
              return fns.reduce((acc, fn) => {
                  return fn(acc);
              }, val)
          }
      }
      
    • 组合函数与管道函数的意义在于:可以把很多小函数组合起来完成更复杂的逻辑。

3、优点

  • 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况;

  • 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响;

  • 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性;

  • 隐性好处:减少代码量,提高维护性。

4、缺点

  • 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销;

  • 资源占用:在 JavaScript 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式;

  • 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作。

4.4、函数缓存

  • 函数缓存,就是将函数运算过的结果进行缓存,本质上就是用空间(缓存存储)换时间(计算过程),常用于缓存数据计算结果和缓存对象,缓存只是一个临时的数据存储,它保存数据,以便将来对该数据的请求能够更快地得到处理。

    const add = (a,b) => a+b;
    const calc = memoize(add); // 函数缓存
    calc(10,20);// 30
    calc(10,20);// 30 缓存
    
  • 实现函数缓存主要依靠闭包、柯里化、高阶函数。

    // 定义
    const memoize = function (func, content) {
      let cache = Object.create(null)
      content = content || this
      return (...key) => {
        if (!cache[key]) {
          cache[key] = func.apply(content, key)
        }
        return cache[key]
      }
    }
    
    // 调用
    const calc = memoize(add);
    const num1 = calc(100,200)
    const num2 = calc(100,200) // 缓存得到的结果
    

    1、在当前函数作用域定义了一个空对象,用于缓存运行结果;

    2、运用柯里化返回一个函数,返回的函数由于闭包特性,可以访问到 cache

    3、然后判断输入参数是不是在 cache 的中。如果已经存在,直接返回 cache 的内容,如果没有存在,使用函数 func 对输入参数求值,然后把结果存储在 cache 中。

  • 虽然使用缓存效率是非常高的,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存。

    以下几种情况下,适合使用缓存:

    • 对于昂贵的函数调用,执行复杂计算的函数;

    • 对于具有有限且高度重复输入范围的函数;

    • 对于具有重复输入值的递归函数;

    • 对于纯函数,即每次使用特定输入调用时返回相同输出的函数。

五、原型链

原型链、继承、Class、this指向改变方法 - 掘金 (juejin.cn)

5.1、new操作符

  • JavaScript 中,new 操作符用于创建一个给定构造函数的实例对象。

    function Person(name, age){
        this.name = name;
        this.age = age;
    }
    Person.prototype.sayName = function () {
        console.log(this.name)
    }
    const person1 = new Person('Tom', 20)
    console.log(person1)  // Person {name: "Tom", age: 20}
    t.sayName() // 'Tom'
    

    从上面可以看到:

    1、new 通过构造函数 Person 创建出来的实例可以访问到构造函数中的属性;

    2、new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)。

    若在构建函数中显式加上返回值,并且这个返回值是一个原始类型,但这个值并没有什么作用;

    function Test(name) {
      this.name = name
      return 1
    }
    const t = new Test('xxx')
    console.log(t.name) // 'xxx'
    

    若在构造函数如果返回值为一个对象,那么这个返回值会被正常使用。

    function Test(name) {
      this.name = name
      console.log(this) // Test { name: 'xxx' }
      return { age: 26 }
    }
    const t = new Test('xxx')
    console.log(t) // { age: 26 }
    console.log(t.name) // 'undefined'
    
  • new 关键字主要做了以下的工作:

    • 创建一个新的对象 obj

    • 将对象与构建函数通过原型链连接起来;

    • 将构建函数中的 this 绑定到新建的对象 obj 上;

    • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理。

  • 手写模拟 new 操作符

    function mynew(Func, ...args) {
        // 1.创建一个新对象
        const obj = {}
        // 2.新对象原型指向构造函数原型对象
        obj.__proto__ = Func.prototype
        // 3.将构建函数的this指向新对象
        let result = Func.apply(obj, args)
        // 4.根据返回值判断
        return result instanceof Object ? result : obj
    }
    

5.2、this对象

​ 函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别,在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。

1、this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象;

2、this在函数执行过程中,this一旦被确定了,就不可以再更改。

1、绑定规则

  • 默认绑定

    • 全局环境中定义 person 函数,内部使用 this 关键字;

      var name = 'Jenny';
      function person() {
          return this.name;
      }
      console.log(person());  //Jenny
      

      1、上述代码输出 Jenny,原因是调用函数的对象在游览器中位 window,因此 this 指向 window,所以输出 Jenny

      2、严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。

  • 隐式绑定

    • 函数还可以作为某个对象的方法调用,这时 this 就指这个上级对象;

      function test() {
        console.log(this.x);
      }
      
      var obj = {};
      obj.x = 1;
      obj.m = test;
      
      obj.m(); // 1
      
    • 这个对象中包含多个对象,尽管这个函数是被最外层的对象所调用,this 指向的也只是它上一级的对象;

      var o = {
          a:10,
          b:{
              fn:function(){
                  console.log(this.a); //undefined
              }
          }
      }
      o.b.fn();
      

      上述代码中,this 的上一级对象为 bb 内部并没有 a 变量的定义,所以输出 undefined

    • 特殊情况

      var o = {
          a:10,
          b:{
              a:12,
              fn:function(){
                  console.log(this.a); //undefined
                  console.log(this); //window
              }
          }
      }
      var j = o.b.fn;
      j();
      

      此时 this 指向的是 window,这里需要记住,this 永远指向的是最后调用它的对象,虽然 fn 是对象 b 的方法,但是 fn 赋值给 j 时候并没有执行,所以最终指向 window

  • new 绑定

  • 显示绑定

    原型链、继承、Class、this指向改变方法 - 掘金 (juejin.cn)

2、箭头函数

ES6 的语法中还提供了箭头函语法,让我们在代码书写时就能确定 this 的指向(编译时绑定)。

const obj = {
  sayThis: () => {
    console.log(this);
  }
};
// window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了
obj.sayThis(); 
const globalSay = obj.sayThis;
globalSay(); // window 浏览器中的 global 对象

虽然箭头函数的 this 能够在编译的时候就确定了 this 的指向,但也需要注意一些潜在的坑。

  • 绑定事件监听;

    const button = document.getElementById('mngb');
    button.addEventListener('click', ()=> {
        console.log(this === window) // true
        this.innerHTML = 'clicked button'
    })
    

    上述可以看到,我们其实是想要 this 为点击的 button,但此时 this 指向了 window

  • 在原型上添加方法时候,此时 this 指向 window

    Cat.prototype.sayName = () => {
        console.log(this === window) //true
        return this.name
    }
    const cat = new Cat('mm');
    cat.sayName()
    
  • 箭头函数不能作为构建函数。

3、优先级

new 绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

六、DOM

​ 文档对象模型(DOM)是 HTML 和 XML 文档的编程接口,它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。

​ 任何 HTML 或 XML 文档都可以用 DOM 表示为一个由节点构成的层级结构,节点分很多类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法,而且与其他类型有某种关系。

6.1、DOM常见操作

1、创建节点

  • createElement

    创建新元素,接受一个参数,即要创建元素的标签名。

    const divEl = document.createElement("div");
    
  • createTextNode

    创建一个文本节点。

    const textEl = document.createTextNode("content");
    
  • createDocumentFragment

    用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到 DOM 中。

    const fragment = document.createDocumentFragment();
    

    当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。

  • createAttribute

    创建属性节点,可以是自定义属性。

    const dataAttribute = document.createAttribute('custom');
    consle.log(dataAttribute);
    

2、查询节点

  • querySelector

    传入任何有效的css 选择器,即可选中单个 DOM 元素(首个):

    document.querySelector('.element')
    document.querySelector('#element')
    document.querySelector('div')
    document.querySelector('[name="username"]')
    document.querySelector('div + p > span')
    

    如果页面上没有指定的元素时,返回 null

  • querySelectorAll

    返回一个包含节点子树内所有与之相匹配的 Element 节点列表,如果没有相匹配的,则返回一个空节点列表。

    const nodeList = document.querySelectorAll("p");
    

    需要注意的是,该方法返回的是一个 NodeList 的静态实例,它是一个静态的“快照”,而非“实时”的查询

    document.getElementById('id属性值'); // 返回拥有指定id的对象的引用
    document.getElementsByClassName('class属性值'); // 返回拥有指定class的对象集合
    document.getElementsByTagName('标签名'); // 返回拥有指定标签名的对象集合
    document.getElementsByName('name属性值'); // 返回拥有指定名称的对象结合
    document/element.querySelector('CSS选择器'); // 仅返回第一个匹配的元素
    document/element.querySelectorAll('CSS选择器'); // 返回所有匹配的元素
    document.documentElement; // 获取页面中的HTML标签
    document.body; // 获取页面中的BODY标签
    document.all['']; // 获取页面中的所有元素节点的对象集合型
    

    除此之外,每个 DOM 元素还有 parentNodechildNodesfirstChildlastChildnextSiblingpreviousSibling 属性。

3、更新节点

  • innerHTML

    不但可以修改一个 DOM 节点的文本内容,还可以直接通过 HTML 片段修改 DOM 节点内部的子树。

    // 获取<p id="p">...</p >
    var p = document.getElementById('p');
    // 设置文本为abc:
    p.innerHTML = 'ABC'; // <p id="p">ABC</p >
    // 设置HTML:
    p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
    // <p>...</p >的内部结构已修改
    
  • innerTexttextContent

    自动对字符串进行HTML编码,保证无法设置任何 HTML 标签。

    // 获取<p id="p-id">...</p >
    var p = document.getElementById('p-id');
    // 设置文本:
    p.innerText = '<script>alert("Hi")</script>';
    // HTML被自动编码,无法设置一个<script>节点:
    // <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p >
    

    两者的区别在于读取属性时,innerText 不返回隐藏元素的文本,而 textContent 返回所有文本。

  • style

    DOM 节点的 style 属性对应所有的 CSS ,可以直接获取或设置。遇到-需要转化为驼峰命名。

    // 获取<p id="p-id">...</p >
    const p = document.getElementById('p-id');
    // 设置CSS:
    p.style.color = '#ff0000';
    p.style.fontSize = '20px'; // 驼峰命名
    p.style.paddingTop = '2em';
    

4、添加节点

  • innerHTML

    如果这个 DOM 节点是空的,例如,<div></div>,那么,直接使用 innerHTML = '<span>child</span>' 就可以修改 DOM 节点的内容,相当于添加了新的 DOM 节点。

    如果这个DOM节点不是空的,那就不能这么做,因为 innerHTML 会直接替换掉原来的所有子节点。

  • appendChild

    把一个子节点添加到父节点的最后一个子节点。通常是先新增一个节点然后。

  • insertBefore

    把子节点插入到指定的位置,使用方法如下:

    parentElement.insertBefore(newElement, referenceElement)
    

    子节点会插入到 referenceElement 之前。

  • setAttribute

    在指定元素中添加一个属性节点,如果元素中已有该属性改变属性值。

    const div = document.getElementById('id')
    div.setAttribute('class', 'white');//第一个参数属性名,第二个参数属性值。
    

5、删除节点

  • 删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的 removeChild 把自己删掉。

    // 拿到待删除节点:
    const self = document.getElementById('to-be-removed');
    // 拿到父节点:
    const parent = self.parentElement;
    // 删除:
    const removed = parent.removeChild(self);
    removed === self; // true
    

    删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置。

6.2、事件模型

1、原始事件模型(DOM0级)

事件绑定监听函数

  • HTML代码中直接绑定;

    <input type="button" onclick="fun()">
    
  • 通过 JS 代码绑定。

    var btn = document.getElementById('.btn');
    btn.onclick = fun;
    

特性

  • 绑定速度快;

    DOM0 级事件具有很好的跨浏览器优势,会以最快的速度绑定,但由于绑定速度太快,可能页面还未完全加载出来,以至于事件可能无法正常运行。

  • 只支持冒泡,不支持捕获;

  • 同一个类型的事件只能绑定一次;

  • 删除 DOM0 级事件处理程序只要将对应事件属性置为 null 即可。

2、标准事件模型(DOM2级)

在该事件模型中,一次事件共有三个过程

  • 事件捕获阶段:事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行;

  • 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数;

  • 事件冒泡阶段:事件从目标元素冒泡到 document ,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。

事件绑定与移除

addEventListener(eventType, handler, useCapture) // 事件绑定
removeEventListener(eventType, handler, useCapture) // 事件移除
  • eventType 指定事件类型(不要加on);

  • handler 是事件处理函数;

  • useCapture 是一个 boolean 用于指定是否在捕获阶段进行处理,一般设置为 false 与IE浏览器保持一致。

var btn = document.getElementById('.btn');
btn.addEventListener(‘click’, showMessage, false);
btn.removeEventListener(‘click’, showMessage, false);

特性

  • 可以在一个 DOM 元素上绑定多个事件处理器,各自并不会冲突;

    btn.addEventListener(‘click’, showMessage1, false);
    btn.addEventListener(‘click’, showMessage2, false);
    btn.addEventListener(‘click’, showMessage3, false);
    
  • 执行时机,当第三个参数设置为 true 就在捕获过程中执行,反之在冒泡过程中执行处理函数。

3、IE事件模型(基本不用)

IE事件模型两个过程

  • 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数;

  • 事件冒泡阶段:事件从目标元素冒泡到document, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行。

事件绑定与移除

attachEvent(eventType, handler) // 事件绑定
detachEvent(eventType, handler) // 事件移除

6.3、事件代理

​ 事件代理,俗地来讲,就是把一个元素响应事件(clickkeydown......)的函数委托到另一个元素,事件流的都会经过三个阶段: 捕获阶段 -> 目标阶段 -> 冒泡阶段,而事件委托就是在冒泡阶段完成;

​ 事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素,当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。

应用场景

​ 如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件,如果给每个列表项都绑定一个函数,那对于内存消耗是非常大的,这时候就可以事件委托,把点击事件绑定在父级元素 ul 上面,然后执行事件的时候再去匹配目标元素。

适合事件委托的事件有:clickmousedownmouseupkeydownkeyupkeypress

事件委托优点

  • 减少整个页面所需的内存,提升整体性能;

  • 动态绑定,减少重复工作。

事件委托缺点

  • focusblur这些事件没有事件冒泡机制,所以无法进行委托绑定事件;

  • mousemovemouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。

如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件。

七、BOM

BOM (Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象。

其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率。

浏览器的全部内容可以看成 DOM,整个浏览器可以看成 BOM。区别如下:

  • DOM
    • 文档对象模型;

    • DOM 就是把文档当作一个对象来看待;

    • DOM 的顶级对象是 DOCUMENT;

    • DOM 主要是操作页面元素;

    • DOM 是 W3C 标准规范。

  • BOM
    • 浏览器对象模型;

    • 把浏览器当作一个对象来看待;

    • BOM 的顶级对象是 WINDOW;

    • BOM 主要是与浏览器窗口进行交互;

    • BOM 是浏览器厂商在各自浏览器上定义的,兼容性较差。

7.1、BOM对象

1、window

BOM 的核心对象是 window,它表示浏览器的一个实例。在浏览器中,window 对象有双重角色,即是浏览器窗口的一个接口,又是全局对象,因此所有在全局作用域中声明的变量、函数都会变成 window 对象的属性和方法。

窗口控制方法:

  • moveBy(x, y):从当前位置水平移动窗体 x 个像素,垂直移动窗体 y 个像素,x 为负数,将向左移动窗体,y 为负数,将向上移动窗体;

  • moveTo(x, y):移动窗体左上角到相对于屏幕左上角的 (x, y) 点;

  • resizeBy(w, h):相对窗体当前的大小,宽度调整 w 个像素,高度调整 h 个像素。如果参数为负值,将缩小窗体,反之扩大窗体;

  • resizeTo(w, h):把窗体宽度调整为 w 个像素,高度调整为 h 个像素;

  • scrollTo(x, y):如果有滚动条,将横向滚动条移动到相对于窗体宽度为 x 个像素的位置,将纵向滚动条移动到相对于窗体高度为 y 个像素的位置;

  • scrollBy(x, y): 如果有滚动条,将横向滚动条向左移动 x 个像素,将纵向滚动条向下移动 y 个像素。

window.open() 既可以导航到一个特定的 url,也可以打开一个新的浏览器窗口。

  • 如果 window.open() 传递了第二个参数,且该参数是已有窗口或者框架的名称,那么就会在目标窗口加载第一个参数指定的 URL;

  • window.open() 会返回新窗口的引用,也就是新窗口的 window 对象;

  • window.close() 仅用于通过 window.open() 打开的窗口,新创建的 window 对象有一个 opener 属性,该属性指向打开他的原始窗口对象。

2、location

http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents
属性名例子说明
hash#contentsurl中#后面的字符,没有则返回空串
hostwww.wrox.com:80服务器名称和端口号
hostnamewww.wrox.com域名,不带端口号
hrefhttp://www.wrox.com:80/WileyCDA/?q=javascript#contents完整url
pathname/WileyCDA/服务器下面的文件路径
port80url的端口号,没有则为空
protocolhttp:使用的协议
search?q=javascripturl的查询字符串,通常为?后面的内容

除了 hash 之外,只要修改 location 的一个属性,就会导致页面重新加载新 URL;

location.reload(),此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载,如果要强制从服务器中重新加载,传递一个参数 true 即可。

3、navigator

navigator 对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂。

4、screen

保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度。

5、history

history 对象主要用来操作浏览器 URL 的历史记录,可以通过参数向前,向后,或者向指定 URL 跳转。

常用的属性如下:

  • history.go()

    • 接收一个整数数字或者字符串参数:向最近的一个记录中包含指定字符串的页面跳转,当参数为整数数字的时候,正数表示向前跳转指定的页面,负数为向后跳转指定的页面。

      history.go('maixaofei.com');
      history.go(3); //向前跳转三个记录
      history.go(-1); //向后跳转一个记录
      
  • history.forward():向前跳转一个页面;

  • history.back():向后跳转一个页面;

  • history.length:获取历史记录数。

7.2、本地存储

1、cookie

  • Cookie 类型为小型文本文件,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP 无状态导致的问题;

  • 作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie 有效期、安全性、使用范围的可选属性组成;

  • 但是 cookie 在每次请求中都会被发送,如果不使用 HTTPS 并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使用 cookie 保持登录态的网站上,如果 cookie 被窃取,他人很容易利用你的 cookie 来假扮成你登录网站。

cookie 常用属性

  • Expires 用于设置 Cookie 的过期时间;

    Expires=Wed, 21 Oct 2015 07:28:00 GMT
    
  • Max-Age 用于设置在 Cookie 失效之前需要经过的秒数(优先级比 Expires 高);

    Max-Age=604800
    
  • Domain 指定了 Cookie 可以送达的主机名;

  • Path 指定了一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部;

    Path=/docs   # /docs/Web/ 下的资源会带 Cookie 首部
    
  • 标记为 SecureCookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。

我们可以看到 cookie 一开始的作用并不是为了缓存而设计出来,只是借用了 cookie 的特性实现缓存。

cookie 的使用

  • document.cookie = '名字=值';

  • 关于 cookie 的修改,首先要确定 domainpath 属性都是相同的才可以,其中有一个不同得时候都会创建出一个新的 cookie

    Set-Cookie:name=aa; domain=aa.net; path=/  # 服务端设置
    document.cookie =name=bb; domain=aa.net; path=/  # 客户端设置
    
  • 最后 cookie 的删除,最常用的方法就是给 cookie 设置一个过期的事件,这样 cookie 过期后会被浏览器删除。

2、localStorage

HTML5 新方法,IE8 及以上浏览器都兼容。

特点

  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的;

  • 存储的信息在同一域中是共享的;

  • 当本页操作(新增、修改、删除)了 localStorage 的时候,本页面不会触发 storage 事件,但是别的页面会触发 storage 事件;

  • 大小:5M(跟浏览器厂商有关系);

  • localStorage 本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡;

  • 受同源策略的限制。

使用

// 设置
localStorage.setItem('username','cfangxu');
// 获取
localStorage.getItem('username');
// 获取第一个键名
localStorage.key(0);
// 删除
localStorage.removeItem('username');
// 一次性清除所有存储
localStorage.clear();

缺点

  • 无法像 Cookie 一样设置过期时间;

  • 只能存入字符串,无法直接存对象。

    localStorage.setItem('key', {name: 'value'});
    console.log(localStorage.getItem('key')); // '[object, Object]'
    

3、sessionStorage

sessionStoragelocalStorage 使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据。

4、indexedDB

indexedDB 是一种低级 API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该 API 使用索引来实现对该数据的高性能搜索,虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB 提供了一个解决方案。

优点

  • 储存量理论上没有上限;

  • 所有操作都是异步的,相比 LocalStorage 同步操作性能更高,尤其是数据量较大时;

  • 原生支持储存 JS 的对象;

  • 是个正经的数据库,意味着数据库能干的事它都能干。

缺点

  • 操作非常繁琐;

  • 本身有一定门槛;

关于 indexedDB 的使用基本使用步骤如下:

  • 打开数据库并且开始一个事务;

  • 创建一个 object store

  • 构建一个请求来执行一些数据库操作,像增加或提取数据等;

  • 通过监听正确类型的 DOM 事件以等待操作完成;

  • 在操作结果上进行一些操作(可以在 request 对象中找到)。