我个人在2022年9月份的笔记的笔记,不定期更新

具有相对特性的无依赖 absolute 绝对定位

请回答下面这个问题:一个绝对定位元素,没有任何 left/top/right/bottom 属性设置,并且其祖先元素全部都是非定位元素,其位置在哪里?

很多人都认为是在浏览器窗口左上方。实际上,还是当前位置,不是在浏览器左上方

这是关于 absolute 绝对定位最典型的错误认知。正是这种错误认知导致凡是使用absolute 绝对定位的地方,一定父容器 position:relative,同时 left/top 等属性定位,甚至必同时使用 z-index 属性设置层级。 在我看来,这是非常严重的认知和使用错误!

请牢记下面这句话:absolute 是非常独立的 CSS 属性值,其样式和行为表现不依赖其他任何 CSS 属性就可以成。

我们来看一个例子感受一下。下图左上角有一个紫色的盒子,请问如何布局?

warn

想必很多人是这么实现的:

.father {
 position: relative;
}
.shape {
 position: absolute;
 top: 0; left: 0;
} 

如果你也是这么实现的,就要注意了,因为这说明你对 absolute 认知还是太浅薄了。实际上,只用下面一行 CSS 就足够了:

.shape {
 position: absolute;
} 

看到没?absolute 定位效果实现完全不需要父元素设置 position为 relative 或其他什么属性就可以实现,我把这种没有设置 left/top/right/bottom 属性值的绝对定位称为 “无依赖绝对定位”。

很多场景下,“无依赖绝对定位”要比使用 left/top 之类属性定位实用和强大很多,因 为其除了代码更简洁外,还有一个很棒的特性,就是“相对定位特性”。

明明 absolute 是‘绝对定位’的意思,怎么又扯到‘相对定位特性’了呢?没错,“无 依赖绝对定位”本质上就是“相对定位”,仅仅是不占据 CSS 流的尺寸空间而已。

看到这里,你可又突发奇想,如果我在最外面在嵌套一个相对定位的盒子,结果又如何呢?

 .grand {
        position: relative;
        width: 500px;
        height: 500px;
        background-color: blue;
        padding: 20px;
      }
  .wrapper {
        width: 200px;
        height: 200px;
        border: 2px solid red;
      }
 .inner {
        position: absolute;
        width: 50px;
        height: 50px;
        background-color: blueviolet;
      }

<div class="grand">
      <div class="wrapper">
        <div class="inner"></div>
      </div>
</div>

你是不是认为就跑到grand的左上角去了?

实际上,它的位置并无任何变化。因为无依赖的绝对定位之和它的父元素绑定。

Node下对于ES Modules的支持

ES Modules,在Node.js v13.2.0以后的版本中可行

ES Modules的向下兼容

在 Node.js 环境中,ES Modules 向下兼容 CommonJS,因此用import方式可以引入 CommonJS 方式导出的 API,但是会以 default 方式引入。

因此以下写法:

// abc.js 这是一个 CommonJS 规范的模块
const a = 1;
const b = 2;
const c = () => a + b;

module.exports = {a, b, c};

它可以用ES Modules的import引入:

import abc from './test.js';
console.log(abc.a, abc.b, abc.c()); // 1 2 3

但是不能用

import {a, b, c} from './test.js';

因为 module.exports = {a, b, c} 相当于:

const abc = {a, b, c};
export default abc;

ES Modules与CommonJS的主要区别

ES Modules与CommonJS的主要有四个区别。

第一个区别前面也提到过,如果要在导出时使用别名,ES Modules要写成:

export {
  a as d,
  b as e,
  c as f,
}

而对应的CommonJS的写法是:

module.exports = {
  d: a,
  e: b,
  f: c,
}

第二个区别是,CommonJS在require文件的时候采用文件路径,并且可以忽略 .js 文件扩展名。也就是说,require(‘./ziyue.js’)也可以写成require(‘./ziyue’)。但是,ES Modules在import的时候采用的是 URL 规范就不能省略文件的扩展名,而必须写成完整的文件名import {ziyue} from ‘./ziyue.mjs’,.mjs的扩展名不能省略。

💡如果你使用 Babel 编译的方式将ES Modules编译成CommonJS,因为 Babel 自己做了处理,所以可以省略文件扩展名,但是根据规范还是应该保留文件扩展名。

第三个区别是,ES Modules的import和export都只能写在最外层,不能放在块级作用域或函数作用域中。比如:

if(condition) {
  import {a} from './foo';
} else {
  import {a} from './bar';
}

这样的写法,在ES Modules中是不被允许的。但是,像下面这样写在CommonJS中是被允许的:

let api;
if(condition) {
  api = require('./foo');
} else {
  api = require('./bar');
}

事实上,CommonJS 的 require 可以写在任何语句块中。

第四个区别是,require 是一个函数调用,路径是参数字符串,它可以动态拼接,比如:

const libPath = ENV.supportES6 ? './es6/' : './';
const myLib = require(`${libPath}mylib.js`);

但是ES Modules的import语句是不允许用动态路径的。

对于Node fs的一些补充

fs.readFile(path[, options], callback)

warn

  1. 若不指定编码格式,则返回一个Buffer(以16进制输出,以二进制格式存储的文件流对象)
  2. 若options是字符串,则它表示的输出编码格式
fs.readFile('/etc/passwd', 'utf8', callback);

文件路径问题

warn

开局一张图,没得说。

在 bullit-generator-js 文件下运行 node index.js肯定是一切正常。 那我如果上一层目录运行呢?

cd ..
E:\study\node> node .\bullit-generator-js\index.js

哦豁,报错了 warn

这是因为,我们使用的相对路径./corpus/data.json是相对于脚本的运行目录(即,node执行脚本的目录),而不是脚本文件的目录。

所以当我们在bullshit_generator当前目录运行时,读取的文件路径是bulhit_generator目录下的/corpus/data.json,这没有问题。如果我们在上一级目录运行它时,读取的文件路径实际变成了../bulhit_generator目录下的/corpus/data.json,因为这个路径下文件不存在,这样就找不到文件了。

这也就意味着,如果使用相对路径./,我们在不同的目录下运行脚本命令, ./corpus/data.json实际上表示的是不同的文件路径。

要让这个命令在任何目录下运行都能正确找到文件,我们必须要修改路径的方式,从相对于脚本运行的目录改为相对于脚本文件的目录。

在commonjs规范下:

const path = resolve(__dirname, 'corpus/data.json')
const data = readFileSync(path, {encoding: 'utf-8'});
console.log(data);

现在我们使用的ES Module 规范,怎么搞?

import {readFileSync} from 'fs';
import {fileURLToPath} from 'url';
import {dirname, resolve} from 'path';

const url = import.meta.url; // 获取当前脚本文件的url
const path = resolve(dirname(fileURLToPath(url)), 'corpus/data.json');
const data = readFileSync(path, {encoding: 'utf-8'});
console.log(data);

url是 Node.js 的内置模块,用来解析 url 地址。fileURLToPath是这个模块的方法,可以将 url 转为文件路径。然后再通过内置模块path的dirname方法就可以取到当前 JS 文件目录。

如何获取命令行中用户输入的参数?

我们首先想到的肯定是process.argv。

第一种方法

function parseOptions(options = {}) {
  const argv = process.argv;
  for (let i = 2; i < argv.length; i++) {
    const cmd = argv[i - 1];
    const value = argv[i];
    if (cmd === "--title") {
      options.title = value;
    } else if (cmd === "--min") {
      options.min = Number(value);
    } else if (cmd === "--max") {
      options.max = Number(value);
    }
  }
  return options;
}

方式二

内置的 process 模块无法方便地检查用户的输入,所以我们需要使用三方库 command-line-args 替代 process.argv,它不仅能获得用户的输入,还能检测用户的输入是否正确。

使用三方库 command-line-usage 输出帮助说明

import commandLineArgs from "command-line-args";
import commandLineUsage from "command-line-usage";
const optionDefinitions = [
  { name: "help" },
  { name: "title", alias: "t", type: String },
  { name: "min", type: Number },
  { name: "max", type: Number },
];

const options = commandLineArgs(optionDefinitions); // 获取命令行的输入

// 定义帮助的内容
const sections = [
  {
    header: "主题",
    content: "for your test ",
  },
  {
    header: "Options",
    optionList: [
      {
        name: "title",
        typeLabel: "{underline string}",
        description: "文章的主题。",
      },
      {
        name: "min",
        typeLabel: "{underline number}",
        description: "文章最小字数。",
      },
      {
        name: "max",
        typeLabel: "{underline number}",
        description: "文章最大字数。",
      },
    ],
  },
];
const usage = commandLineUsage(sections); // 生成帮助文本

if ("help" in options) {
  console.log(usage);
  process.exit();
}

export { options };

关于JS中函数参数传递的几个题

基本数据作为函数参数

let a = 1;
function fna(a) {
  a = 2;
  console.log(a, "inner a");
}
fna(a);
console.log(a, "outter a");

毫无疑问、相当于拷贝一份,互不相干

引用数据作为函数参数

未改变指针指向的情况下,指向的是同一内存空间

let a = { x: 1 };
function fna(a) {
  a.x = 2;
  a.y = 2;
  console.log(a, "inner a");
}
fna(a);
console.log(a, "outter a");

// { x: 2, y: 2 } inner a
// { x: 2, y: 2 } outter a

改变了指针指向的情况下,创建一个新的对象

let a = { x: 1 };
function fna(a) {
  a = { b: 3 };
  console.log(a, "inner a");
}
fna(a);
console.log(a, "outter a");

// { b: 3 } inner a
// { x: 1 } outter a

关于Node中ReadableStream的一些看法

  • stream Node.js 流(stream):你需要知道的一切

推荐先看上文。

这些内置对象都实现了Stream类

warn

如何用 stdin 和 readline 实现命令行交互?

用户的输入与监听

在命令行运行时,通过 process 模块的 stdin 对象可以获取用户输入

console.log("请输入一个要求和的整数,以0结束输入");

process.stdin.setEncoding("utf8");

let sum = 0;
process.stdin.on("readable", () => {
  const chunk = process.stdin.read(); // 获取当前输入的字符,包含回车
  const n = Number(chunk.slice(0, -1));
  sum += n;
  if (n === 0) process.stdin.emit("end");
  process.stdin.read(); // 再调用一次,返回的是null,并继续监听
});

process.stdin.on("end", () => {
  console.log(`求和结果是:${sum}`);
});

使用 readline 模块

用 process.stdin 实现命令行交互,需要在readable事件中多调一次process.stdin.read()方法,这看起来似乎很奇怪,代码的可读性不高。幸好,node.js 为我们提供了一个比 process.stdin 更好用的内置模块—— readline,它是专门用来实现可交互命令行的。

import readline from 'readline';
function question(rl, {text, value}) {
  const q = `${text}(${value})\n`;
  return new Promise((resolve) => {
    rl.question(q, (answer) => {
      resolve(answer || value);
    });
  });
}

export async function interact(questions) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  const answers = [];
  for(let i = 0; i < questions.length; i++) {
    const q = questions[i];
    const answer = await question(rl, q); // 等待问题的输入
    answers.push(answer);
  }
  rl.close();
  return answers;
}