本文记录了我学习过程中整理的关于JavaScript的知识点,以摘抄为主,绝大部分非原创。未能全部都标明出处,在此致歉

历史

双等号和三等号和Object.is

  • == 只比较值是否相等。在比较数值之前,它将变量的类型转换为相互匹配
  • === 不执行类型转换。它将验证被比较的变量是否具有相同的值和相同的类型。
  • Object.is()
    • 与 == 运算符并不等价: Object.is() 不会对其操作数进行类型转换
    • 也不等价于 === 运算符: 唯一区别在于它们处理带符号的 0 和 NaN 值的时候。=== 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等

示例

const number = 1234 
const stringNumber = '1234'  

console.log(number == stringNumber) //true
console.log(number === stringNumber)  //false  

console.log(0 == false) //true
console.log(0 === false) //false 

const str = ""

console.log(str == false) //true
console.log(str === false) //false

参考


? and ??

  • ? 可选链操作符(Optional Chaining Operator)
    • 用于简化访问嵌套对象属性时的代码,可以安全地访问深度嵌套的属性,避免因为中间属性不存在而导致的错误。
  • ?? 空值合并操作符(Nullish Coalescing Operator),用于提供默认值
    • 当左侧操作数为null或undefined时,返回右侧操作数,否则返回左侧操作数。
    • 与逻辑或(||)操作符不同,空值合并操作符只在左侧操作数为null或undefined时返回右侧操作数,对于其他假值(如0、‘‘等)不会触发返回右侧操作数。
    • ?? "" is a Code Smell / Jordan Eldredge

静态属性和静态方法

  • 静态方法用于实现属于整个类,但不属于该类任何实例的函数。
    class Article {
      constructor(title, date) {
        this.title = title;
        this.date = date;
      }
    
      // 静态方法
      static compare(articleA, articleB) {
        return articleA.date - articleB.date;
      }
    }
    // 直接将其作为属性赋值的作用相同
    Article.compare = function (articleA, articleB) {
        return articleA.date - articleB.date;
    }
    // 可以在类上调用,而不是在单个对象上
    let article = new Article("HTML", new Date(2019, 1, 1));
    article.compare() /// Error: article.createTodays is not a function
    
  • 静态属性
    • 等同于直接给类赋值
  • 静态属性和方法是可被继承的。
  • 参考

私有属性

  • 私有属性通过添加#前缀来创建,在类的外部无法合法地引用。
  • 私有属性不是原型继承模型的一部分,因为它们只能在当前类内部被访问,而且不能被子类继承。
  • 无法使用方括号表示法动态访问它
  • 访问对象中不存在的私有属性,会抛出 TypeError 错误,而不是像普通属性一样返回 undefined
  • 参考私有属性 - JavaScript | MDN

序列化与反序列化

相等性

摘自真的不可以在 React 组件内部嵌套定义子组件吗? - PRIN BLOG

  • 值相等性 (Value Equality),即两个值类型一致,内容也一致
  • 引用相等性 (Reference Equality),即两个对象的引用在内存中指向同一块区域
    // 两个长得一样的对象
    const a = { name: 'Anon Tokyo' };
    const b = { name: 'Anon Tokyo' };
    
    // "引用相等性" 比较 - false
    console.log(a === b);
    console.log(Object.is(a, b));
    
    // "值相等性" 比较 - true
    console.log(lodash.isEqual(a, b));
    console.log(a.name === b.name);
    
  • React 在比较节点的 type 时,使用的是===严格相等。也就是"引用相等性"

CommonJS V.S. ES6(ECMAScript 6)

参考CommonJS vs. ES modules in Node.js - LogRocket Blog

二者都是JavaScript的模块规范,现代开发中后者用的比较多。

模块化


IIFE(立即调用函数表达式)

  • 参考IIFE(立即调用函数表达式)| MDN
  • 一种设计模式
  • 示例:
    var foo = 'hello';
    (function(foo){
      console.log(foo);
      var foo = foo || 'world';
      console.log(foo);
    })(foo);
    console.log(foo);
    // 依次输出 hello hello hello
    

作用域,执行上下文,词法环境

wip

参考

闭包

概念

  • 闭包是由函数以及声明该函数的词法环境组合而成的,该环境包含了这个闭包创建时作用域内的任何局部变量。

示例:

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

作用一: 模拟私有方法

  • 代码
    var makeCounter = function () {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function () {
          changeBy(1);
        },
        decrement: function () {
          changeBy(-1);
        },
        value: function () {
          return privateCounter;
        },
      };
    };
    
    var Counter1 = makeCounter();
    var Counter2 = makeCounter();
    console.log(Counter1.value()); /* logs 0 */
    Counter1.increment();
    Counter1.increment();
    console.log(Counter1.value()); /* logs 2 */
    Counter1.decrement();
    console.log(Counter1.value()); /* logs 1 */
    console.log(Counter2.value()); /* logs 0 */
    
  • 现在可以通过在属性名之前添加# 前缀来创建,在类的外部无法合法地引用

输出题

var a = 100
function create() {
  var a = 200
  return function () {
   alert(a)
  }
}
var fn = create()
fn() //200

var a = 100
function invoke(fn) {
  var a = 200
  fn()
}
function fn() {
  alert(a)
}
invoke(fn) //100

参考


提升

会被提升

  1. 变量的声明会被提升到当前作用域的顶端
  2. 函数声明和初始化都会被提升

不会被提升

  1. 变量的初始化不会被提升
  2. 函数表达式不会被提升

顺序

  1. 函数提升在变量提升之前
  2. 变量被提升过后,先对提升上来的所有对象统一执行一遍声明步骤,然后再对变量执行一次赋值步骤。而执行赋值步骤时,会优先执行函数变量的赋值步骤,再执行普通变量的赋值步骤

示例

  1. 示例一
var foo = 3;
function hoistVariable() {
  var foo = foo || 5;
  console.log(foo); // 5
}
hoistVariable();
  1. 示例二:函数表达式不会被提升
console.log(square); // undefined
console.log(square(5)); // square is not a function =》 初始化并未提升,此时 square 值为 undefined
var square = function (n) { 
  return n * n; 
}
  1. 示例二: 顺序
// 源代码
function b(){};
var b = 11;
typeof b; // number

//预编译后
function b;  // => 声明一个function b
var b;       // =》 声明一个变量 b
b = (){};    // =》 function b 初始化
b = 11;      // =》 变量 b 初始化 =》变量初始化没有被提升,还在原位
typeof b;    // number
  1. 示例三: 顺序
let a =1;
function foo(a){
  return (a=a+1);
}
var b = foo(a);
let c = foo(a);
function foo(a){
  return (a=a+2);
}
const d = foo(a)
console.log(a,b,c,d); //1,3,3,3

参考


Var, Let, Const

作用域

  • var 声明是全局作用域或函数作用域
  • let 和 const 是块作用域。

变化

  • var 变量可以在其作用域内更新和重新声明
  • let 变量可以更新但不能重新声明
  • const 变量既不能更新也不能重新声明。

提升:

  • 它们都被提升到了作用域的顶部。
  • var 变量是用 undefined 初始化的
  • 而 let 和 const 变量不会被初始化,在代码实际执行到声明和初始化语句之前,它们的值是不可访问的。

声明

  • var 和 let 可以在不初始化的情况下声明,而 const 必须在声明时初始化。
  • var可以重复声明
    • 示例
      var haha = '666';
      var haha;
      console.log(haha); // 666
      
    • 原因:
      1. 编译器在对代码进行拆解的时候,遇到了var定义的变量会首先询问作用域中是否存在这个变量
      2. 不存在就让作用域创建变量,如果存在就忽略var继续编译
      3. 赋值时haha=‘666’被执行,第二个var haha被编译器忽略了。

示例:

let greeting = "say Hi";
let greeting = "say Hello instead"; // error: Identifier 'greeting' has already been declared

if (true) {
  var y = 20; // y在if块内部可见
  console.log(y); // 输出: 20
}

console.log(y); // 输出: 20

b = 1;
let b; // Cannot access 'b' before initialization

输出题

      let $body = document.getElemnetById('body')
      let arr = [1,2,3,4,5]
      let i, length = arr.length, domA
      for (i = 0; i < length; i++) {
      domA = document.createElement(<a>${i}</a> )
      $body.append(domA)
      domA.click(function(){
      alert(i)
      })
      }
      // 输出的都是5
  • 由于 i 是在循环外定义的,这里会导致在点击时总是弹出循环结束时的值(5)
  • 可以通过使用闭包或 let 来解决这个问题。

参考


Rest参数 与 Spread 语法

  • 参考Rest 参数与 Spread 语法
  • Rest参数用来从参数列表中获取数组
  • Rest 参数必须放到参数列表的末尾
  • 箭头函数没有 “arguments”
  • spread 语法把数组转换为参数列表
  • 举例:
// Rest参数用来从参数列表中获取数组
let sumAll = (...args) => {
  let sum = 0;
  for(let arg of args)
    sum += arg;
  return sum;
}

sumAll(1,2,3)

// spread 语法把数组转换为参数列表
let array = [0,1,2,3,4]
sumAll(...array)

深拷贝和浅拷贝

数据存储

  1. 基本类型数据保存在在栈内存中
  2. 引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中

浅拷贝

  • 定义: 只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

  • 区别于=:

    • =赋值操作只是复制引用,两个变量指向同一个对象。
    • 浅拷贝创建一个新对象,但对于嵌套的引用类型仍然共享同一个引用。
  • 实现一

    function shallowCopy(obj){
      if(typeof obj !== 'object'){
        return obj;
      }
      let newObj = {};
      for(let key in obj){
        if(obj.hasOwnProperty(key)){
          newObj[key] = obj[key]
        }
      }
      return newObj;
    }
    
  • 实现二

    function shallowCopy(obj){
      return {...obj}
    }
    
  • 实现三

    let obj = {
      a:1,
      b:[1,2,3];
    }
    // Object.assign: 将源对象的可枚举属性复制到目标对象,适合合并多个对象。
    let obj1 = Object.assign({},obj);
    

深拷贝

  • 定义: 创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响原对象
  • 实现一
    • 代码
      function deepCopy(obj){
        if(typeof obj !== 'object'){
          return obj;
        }
        let newObj = obj instanceof Array ? [] : {};
        for(let key in obj){
          if(obj.hasOwnProperty(key)){
            newObj[key] = deepCopy(obj[key]);
          }
        }
        return obj;
      }
      
    • 缺陷:
      1. 无法处理循环引用
      2. 递归爆栈
  • 实现二:
    • 代码
function cloneForce(x){
    const uniqueList = []; // 用来去重
    let root = {};
    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length){
        // dfs
        const node = loopList.pop();
        const {parent, key, data} = node;

        // 初始化赋值目标
        let res = parent;
        if(typeof key !== 'undefined'){
            res = parent[key] = {};
        }
        let uniqueData = find(uniqueList, data);
        if(uniqueData){
            // 数据已经存在
            parent[key] = uniqueData.target;
            continue;
        }
        // 数据不存在
        uniqueList.push({
            source: data,
            target: res,
        });
        for(let k in data){
            if(data.hasOwnProperty(k)){
                if(typeof data[k] === 'object'){
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                }else{
                    res[k] = data[k];
                }
            }
        }
    }
    return root;
}

function find(arr, item){
    for(let i = 0; i < arr.length; i++){
        if(arr[i].source === item){
            return arr[i];
        }
    }
}
  • clone
    let clone = Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj)
    );
    

structuredClone()

参考


复制


读取剪贴板

wip


JavaScript编程中常见的代码坏味道

wip

Pyramid of doom(厄运金字塔)


Proxy

wip

参考


Reflect

wip

参考

问题

  1. js到dom树的单向绑定

  2. 牛客网ACM模式下JavaScript(V8)常见输入输出练习_readline(…).split is not a function-CSDN博客

  3. JavaScript实现封装、继承、多态JavaScript实现封装、继承、多态 - 掘金

  4. parseInt

    • parseInt() - JavaScript | MDN
    • 示例代码
      let res = ['1', '2', '100'].map(parseInt);
      console.log(res); // [1, NaN, 4]
      
    • 解释
      • 实际上 map 会传递三个参数给 parseInt,而 parseInt 只关心前两个参数:要解析的字符串和进制
      • 因此解析过程如下:
        1. 对第一个元素 ‘1’,parseInt(‘1’, 0),0 表示根据输入自动检测进制,结果是 1。
        2. 对第二个元素 ‘2’,parseInt(‘2’, 1),1 不是有效的进制,因此结果是 NaN。
        3. 对第三个元素 ‘100’,parseInt(‘100’, 2),以二进制解析 ‘100’,结果是 4。
  5. 箭头函数与普通函数的区别

  6. 为什么会出现箭头函数

    • 消除函数的二义性
      • 在出现箭头函数之前,函数可以直接被调用,也可以被new当作构造函数使用,这就是函数的二义性。
      • 为了消除这种二义性,创造了箭头函数和class,箭头函数只能() => {},当作普通函数调用,而class只能被new,这就刚好对应二义性的两种用法,解决这种复杂情况了
    • 解决了this的复杂指向问题
  7. ES6新特性

    • 引入了let和const
    • 模板字符串: ${}
    • 箭头函数
    • 函数参数默认值,Rest参数
    • 解构赋值
    • Promise
    • 新增Map,Set,WeakMap,WeakSet
    • 迭代器和生成器
      let arr =[1,2,3];
      for(let item of arr){
       console.log(item)
      }
      
    • 对象和类
  8. 手撕

  9. 实现继承