0%

介绍

IIFE(Immediately Invoked Function Expression),中文名称:立即执行函数表达式,其实IIFE最早并不叫这个名字,而是叫做Self-Executing Anonymous Function,即自执行匿名函数。根据MDN的资料,IIFE这个说法最早由Ben Alman于2010年提出,下面我们一起来看看这个名字的来龙去脉。

2010年11月5日,Ben Alman写下来他的著名文章:Immediately-Invoked Function Expression (IIFE),标志着IIFE这个名字的诞生。

在文章中,Ben Alman称他是一个对待术语非常严谨的人,之前他多次看到Self-Executing Anonymous Function这个说法,觉得不是很恰当,于是他提出了Immediately-Invoked Function Expression这个说法。

IIFE到底是咋来的?

当我们定义一个函数或者一个函数表达式时,你得到的是一个名字,通过这个名字,你就可以调用这个函数。

下面这两段代码,第一个定义了一个普通函数,第二个定义了一个函数表达式,这两种形式,我们都可以通过标识符foo来调用它们。

1
2
3
4
5
6
7
8
9
// 普通函数
function foo() {
console.log('I am a function');
}

// 函数表达式
const foo = function() {
console.log('I am a function expression');
};

也就是说,当javascript解释器遇到全局function关键字,或者一个函数内部的function关键字时,会将其解释为一个函数声明。

然而函数声明是无法直接调用的,所以下面的写法会导致错误:

1
2
3
function foo() {
console.log('I am a function'); // Uncaught SyntaxError: Unexpected token ')'
}();

我们来分析一下,上面这段代码,javascript解释器会将其解释为一个函数声明,和一个分组操作符(()), 分组操作符是用来改变运算符优先级的,里面必须有表达式才行,所以javascript解释器会报错。

那我们就给它一个表达式:

1
2
3
function foo() {
console.log('I am a function'); // Uncaught SyntaxError: Unexpected token ')'
}(1);

这回代码不报错了,但是这段代码毫无意义,这个函数并没有执行,实际上这段代码与下面的代码等价:

1
2
3
4
5
function foo() {
console.log('I am a function');
}

(1);

它的返回值就是1,这不是我们想要的结果,我们需要函数定义后能立即被执行,那就需要我们告诉javascript解释器,这个函数是一个表达式,而不是一个声明,因为表达式可以立即执行,但是声明不能。

而在javascript中,生成表达式最简单的方式就是用()包裹起来,于是有了下面的代码

1
2
3
(function foo() {
console.log('I am a function');
});

这样函数声明就变成了一个函数表达式,但是这个表达是没有名字,我们没法调用它,我们先给它一个名字,然后通过名字调用它。

1
2
3
4
5
const bar = (function foo() {
console.log('I am a function');
});

bar(); // I am a function

这样完全没有问题,但是这里的bar实在有点多余,实际上bar

1
2
3
(function foo() {
console.log('I am a function');
});

是等价的,既然bar()可以调用函数,那么我们直接在函数表达式末尾加上(),也可以调用这个函数,于是就有了下面的代码,这就是IIFE的由来。

1
2
3
(function foo() {
console.log('I am a function');
})();

()写在外层的括号内也一样,这种方式颇得javascript专家Douglas Crockford的青睐。我本人更喜欢第一种。

1
2
3
(function() {
console.log('I am a function');
}());

IIFE的变种

有上面介绍可知,生成IIFE的精髓就是将函数声明变成函数表达式,而在javascript中,生成表达式可不止使用()包裹起来这一种方式,还有很多其他的方式可以实现。于是IIFE也就是产生了诸多变种。

这个变种利用赋值运算符=来实现,赋值运算符是将右侧表达式的值赋值给左侧变量的,所以它右侧的部分被解析成了函数表达式及其调用。

1
2
3
const i = function() {
console.log('I am an IIFE');
}();

下面的表中使用逻辑运算符来生成表达式。

1
2
3
true & (function() {
console.log('I am an IIFE');
}());

还有下面这些,都是利用一元运算符来生成函数表达式。

1
2
3
4
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();

最后来一个不为人知的,void运算符会对其右侧的表达是求值然后返回undefined。(void expression - 先对expression求值,然后返回undefined)。

1
2
3
void function() {
console.log('I am an IIFE');
}();

还有使用new运算符来生成IIFE,这种方式比较少见,因为它会创建一个新的对象。

1
2
3
new function() {
console.log('I am an IIFE');
}();

这些方式都比较偏门了,不建议使用,只是用来帮助我们理解IIFE的。

为什么Self-Executing Anonymous Function这个名字不好?

Ben Alman认为这个名字有两个问题:

Self-Executing:这个名字暗示函数会调用自己,但是实际上函数是立即被执行的,而不是调用它自身。
比如下面的几段代码都会调用自己,但是这并不是IIFE的语义。

1
2
3
4
5
// 递归调用自身
function foo() { foo();

// 使用arguments.callee调用自身
const foo = function() { arguments.callee(); };

Anonymous:这个名字暗示函数是匿名的,但实际上函数可以有名字,也可以没有名字,比如下面的例子:

1
2
3
4
// 有名字的IIFE
(function foo() {
console.log('I am an IIFE');
})();

参考

https://web.archive.org/web/20171201033208/http://benalman.com/news/2010/11/immediately-invoked-function-expression/#iife

大家有时间可以去拜读Ben Alman的原文,大佬写的东西就是不一样,通俗易懂,是我辈楷模!

介绍

IIFE(Immediately Invoked Function Expression)中文名称:立即执行函数表达式,是一种JavaScript编程模式,它允许函数在定义后立即执行。由这个定义可以看出IIFE有两个特点:

  1. 它是函数表达式
  2. 它是定义后立即执行的

来看一个例子:

1
2
3
(function() {
console.log('I am an IIFE');
})();

将这段代码放在浏览器控制台中执行,会输出I am an IIFE。这就是一个简单的IIFE。

分析一下上面的代码,它分为两部分,每个部分由一组()包裹。
第一部分是一个函数(本例用的是匿名函数,也可以是具名函数):

1
2
3
(function() {
console.log('I am an IIFE');
})

第二部分是一个函数调用

1
();

这两部分结合在一起,就形成了一个IIFE。

快速掌握IIFE

IIFE的语法其实不复杂,但是刚开始要记住它并不容易,这里教大家一个小技巧,可以快速书写IIFE代码。

  1. 首先,写下两组括弧()()
  2. 然后在第一组括弧内写下函数定义 - 注意这里必须是匿名函数。
  3. 如果函数需要参数,那么在第二组括弧内传入参数。

一道练习题,写一个IIFE,计算1 + 2的和

我们使用上面的技巧来书写:

首先写下两组括弧

1
()()

然后在第一组括弧内写下函数定义

1
2
3
(function add(a, b) {
console.log(a + b);
})()

因为这里的add函数需要参数,所以我们要在第二组括弧内传入参数

1
2
3
(function add(a, b) {
console.log(a + b);
})(1, 2);

IIFE的变种

上面介绍的IIFE写法是最常见的形式,其实IIFE还有一些其他写法(至于为什么这些写法是可以的,我们下一篇再做介绍),现罗列如下:

这种写法将调用函数的括号放到了第一组括弧内部。

1
2
3
(function() {
console.log('I am an IIFE');
}());

以下几种写法都是使用一元运算符来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
!function() {
console.log("IIFE with !")
}();

~function() {
console.log("IIFE with ~")
}();

+function() {
console.log("IIFE with +")
}();

-function() {
console.log("IIFE with -")
}();

这些一元运算符的作用是将函数定义转换为一个表达式,从而使其可以被调用。如果不加这些一元运算符的话,就变成了下面的形式:

1
2
3
function() {
console.log("IIFE without unary operator")
}();

这会导致一个语法错误,因为JavaScript引擎会将其解析为一个函数声明,而不是函数表达式。函数声明需要一个函数名,而匿名函数没有名字,所以会报错:

1
Uncaught SyntaxError: Function statements require a function name

我们给它加上一个名字,变成下面这样

1
2
3
function foo() {
console.log("IIFE with function name")
}();

但是还是会报错:

1
Uncaught SyntaxError: Unexpected token ')'

javascript引擎会将上述代码解析为一个函数声明和一个分组操作符 - 即(),但是分组操作符内不能是空的,必须有一个表达式,我们在里面加上一个1

1
2
3
function foo() {
console.log("IIFE with function name")
}(1);

这样就不会报错了,但是这不是一个IIFE了。它的返回值就是分组表达式的值,也就是1。

为什么需要IIFE?

早期在JavaScript尚未支持模块化的时候,IIFE是实现模块化的一种简单方式,它可以创建一个独立的作用域,从而避免全局污染,下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const myModule = (function MyModule() {
const privateVariable = 'private variable';
const publicVariable = 'public variable';

function privateMethod() {
console.log(privateVariable);
}

function publicMethod() {
console.log(publicVariable);
}

// 将需要暴露给外界的变量和方法写在返回的对象中
return {
publicVariable,
publicMethod,
};
})();

console.log(myModule.publicVariable); // 访问公共变量
myModule.publicMethod(); // 访问公共方法

console.log(myModule.privateVariable); // undefined
myModule.privateMethod(); // TypeError: myModule.privateMethod is not a function, privateMethod is not accessible

分析一下上面的代码,这是一个典型的IIFE + 闭包实现模块化的例子。

MyModule是一个IIFE,它在内部定义了:

  • 私有变量privateVariable
  • 私有方法privateMethod

这些变量和方法无法从外部访问。

它还定义了

  • 一个公共变量publicVariable
  • 一个公共方法publicMethod

这些可以通过返回的对象访问。

IIFE的返回值赋值给了myModule变量,这样就可以通过myModule访问公共变量和方法。

注意:这个例子仅供参考,在ES6模块已经十分普及的今天,这种方式已经不推荐使用了。

介绍

今天在写前端性能监控的文章时,需要模拟一个长任务,所谓长任务就是指阻塞主线程超过50毫秒的任务。

这种任务当然直接交给AI做了,我给了AI一个提示:帮我模拟一个执行事件超过50毫秒的JavaScript任务。

于是AI给了以下几个代码片段:

使用Busy-Wait来模拟一个长任务

使用Busy-Wait来模拟一个长任务,这个实现没啥毛病,也是我最终采用的方案,就是阻塞主线程直到50毫秒过去:

1
2
3
4
5
6
function simulateLongTask() {
const start = performance.now();
while (performance.now() - start < 50) {
// 模拟一个长任务
}
}

使用setTimeout来模拟一个长任务

下面这个实现使用setTimeout来模拟一个长任务。

1
2
3
4
5
6
7
function simulate50msTask() {
const start = performance.now();
setTimeout(() => {
const end = performance.now();
console.log(`Task took ~${end - start}ms`);
}, 50);
}

但是这个长任务是有问题的,它不会阻塞主线程,因为setTimeout是异步的,所以它不会在50毫秒内阻塞主线程。

这可能是一个常见的误区吧,setTimeout中的延时参数并不意味着主线程会在这个时间内被阻塞,它只是告诉浏览器在指定的时间后执行回调函数。

结论

所以,如果要模拟一个长任务,不能使用setTimeout,最好的办法就是使用循环(while, for)等待的方式,直到耗尽给定的时间。

介绍

今天我们来谈谈JavaScript中如何做性能监控,性能优化也是前端开发中的老话题了,做好性能优化的前提是要有性能监控,只有知道哪里有性能问题,才能有针对性的进行优化。
性能监控的方式多种多样,比如使用浏览器的开发者工具,使用第三方性能监控工具,或者自己编写代码来监控性能等。

今天我们主要介绍一下从代码层面如何做性能监控,JavaScript中提供了一个PerformanceObserver接口,可以用来监控性能相关的事件。

使用PerformanceObserver进行性能监控

首先我们需要定义一个PerformanceObserver实例。

1
2
3
4
5
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(`${entry.entryType}: ${entry.name} - ${entry.startTime} - ${entry.duration}`);
});
});

PerformanceObserver的构造函数,接受以下三个参数:

  1. entities - 这是一个PerformanceObserverEntryList对象,包含一系列可观察的性能事件。
  2. observer - 接收上述事件的观察者对象。
  3. options - 这是一个可选的配置对象,我们暂时用不到它,先忽略。

接下来,我们需要指定我们要监听的性能事件类型,比如longtaskmark等。

1
observer.observe({ entryTypes: ['longtask', 'mark'] });

可以使用PerformanceObserver中的静态函数PerformanceObserver.supportedEntryTypes来获取支持的性能事件类型。

1
console.log(PerformanceObserver.supportedEntryTypes);

在我的浏览器上,支持以下类型的性能事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
"element"
"event"
"first-input"
"largest-contentful-paint"
"layout-shift"
"long-animation-frame"
"longtask"
"mark"`
"measure"
"navigation"
"paint"
"resource"
"visibility-state"

监控longtask事件

longtask任务是指阻塞主线程超过50毫秒的任务,我们首先来模拟一个longtask任务。

1
2
3
4
function simulateLongTask() {
const start = performance.now();
while (performance.now() - start < 50) {} // 模拟一个50毫秒的任务
}

接下来,我们在PerformanceObserver中监听longtask事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Create a PerformanceObserver to observe long tasks
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log("Long task detected:", entry);
});
});

// Start observing long tasks
observer.observe({ type: "longtask", buffered: true });

// Start the long task.
simulateLongTask();

打开控制台,可以看到如下输出,可以看到entryTypelongtask,表示这是一个长任务。duration表示任务的持续时间为50毫秒,满足长任务的条件。如果将simulateLongTask中的时间改为小于50毫秒,则不会触发longtask事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Long task detected: PerformanceLongTaskTiming 
{
name: "self",
entryType: "longtask",
startTime: 10.299999997019768,
duration: 50,
attribution: [
{
name: "unknown",
entryType: "taskattribution",
startTime: 0,
duration: 0,
containerType: "window",
containerSrc: "",
containerId: "",
containerName: ""
}
]
}

参考

  1. https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver

介绍

今天在写代码的时候,用到了Ant Design的Select组件,也就是下拉选择框,和以前一样,习惯性的给Select组件加上了placeholder,但是运行程序时,发现Select组件中并未显示placeholder。我的代码如下,大家看出问题来了吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Form, Select } from 'antd';

export default function SelectComponent() {
const options: { label: string; value: string }[] = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
];

return (
<Form initialValues={{ select: '' }} layout="vertical">
<Form.Item label="Select an option" name="select">
<Select
options={options}
style={{ width: 200 }}
placeholder="Select an option"
allowClear
/>
</Form.Item>
</Form>
);
}

问题分析

没错,罪魁祸首就是这个initialValues={{ select: '' }},因为我在Form组件中设置了Select组件初始值为'',注意initialValues中小写的select对应Form.Item中组件的name,不要与大写的Select名字搞混了,这就导致该组件的值被设置为空字符串,从而无法显示placeholder

如果把这个initialValues去掉,或者将其设置为undefined,就可以正常显示placeholder了。

反过来思考一下,placeholder的作用是在用户尚未选择某个值的时候给用户一个提示,而空字符串从程序的角度来说,是一个合法的值,从而导致Ant Design的Select组件不会显示placeholder

但是undefined就不一样了,它表示没有值,这时Select组件就会显示placeholder。(还是基础不牢呀)。

源码分析

为了追本溯源,我们从源码的角度分析一下其中细节,首先去Ant Design的github页面将源码下载到本机。https://github.com/ant-design/ant-design

然后找到Select组件的源码,路径是src/components/select。在这个目录下,我们可以找到index.tsx文件。这就是Select组件的入口了。

于是尝试在文件中搜索placeholder,竟然没找到,这时我才意识到,Ant Design的Select组件并没有直接处理placeholder,而是通过rc-select这个库来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>() {
//...
return (
<RcSelect<any, any>
ref={ref as any}
virtual={virtual}
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
{...selectProps}
// ...
/>
);
};

// ...
const Select = React.forwardRef(InternalSelect);
export default Select;

从上面的代码可以看出,InternalSelect组件内容是通过RcSelect来实现的,而RcSelect是一个来自rc-select库的组件。遂又去github上找到这个库的源码 - https://github.com/react-component/select。

全库搜索placeholder,很快找到了相关代码,下面是placeholder对应的结点的定义,最终Select通过渲染这个结点来显示placeholder

1
2
3
4
5
6
7
8
9
10
11
12
13
const placeholderNode = React.useMemo<React.ReactNode>(() => {
if (item) {
return null;
}
return (
<span
className={`${prefixCls}-selection-placeholder`}
style={hasTextInput ? { visibility: 'hidden' } : undefined}
>
{placeholder}
</span>
);
}, [item, hasTextInput, placeholder, prefixCls]);

if(item) - 这句很快被排除,因为item是当前选中的值,而我们要显示placeholder的前提是没有选中任何值。

于是来到style这一行,可以看到如果hasTextInputtrue,则placeholder的样式会被设置为visibility: 'hidden' - 也就是隐藏。

1
style={ hasTextInput ? { visibility: 'hidden' } : undefined }

继续查找hasTextInput的定义,发现它是通过inputValue来判断的。

1
const hasTextInput = mode !== 'combobox' && !open && !showSearch ? false : !!inputValue;

继续查找inputValue的定义,最终发现如下代码,看到了吗?这里添加了空字符串,也就是说如果inputValue是空字符串,那么hasTextInput就会被设置为true,从而导致placeholder被隐藏。

1
let inputValue: string = searchValue || '';

打完收工,觉得有用就点个关注,你的关注是我输出的动力!我们明天再见!

介绍

CSS中如何实现继承(扩展)呢?很多编程语言都支持继承(或扩展),在一个基类的基础上添加新的属性或者方法,比如下面是JavaScript中的继承(扩展)语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
constructor() {
this.baseProperty = 'base';
}
baseMethod() {
console.log('base method');
}
}

class Extended extends Base {
constructor() {
super(); // 调用基类的构造函数
this.extendedProperty = 'extended';
}
extendedMethod() {
console.log('extended method');
}
}

通过继承,我们可以把共同的属性或者方法抽象到基类中,然后在子类中添加新的属性或者方法。那么问题来了,CSS中是否也有类似的语法来实现继承(扩展)呢?

原生CSS中的继承

很遗憾,原生CSS并没有提供类似于编程语言中的继承(扩展)语法。早期CSS曾出现过@apply语法,但是现在这个提案已经被废弃了,不推荐使用。

虽然原生CSS没有语法层面的支持,但是我们可以采用多个选择器的方式来实现这种效果,假设我们有三个button,分别时normalwarningerror,这三种button除了颜色不一样之前,其他属性诸如宽度,高度、边框、字体等都是相同的,我们可以这样写:

先定义定义公共的button样式:

1
2
3
4
5
6
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

然后为三种button分别指定颜色:

1
2
3
4
5
6
7
8
9
.normal-button {
background-color: #4caf50; /* Green */
}
.warning-button {
background-color: #ff9800; /* Orange */
}
.error-button {
background-color: #f44336; /* Red */
}

最后使用这些样式的时候,为每个button都指定.button类和他们各自的颜色类:

1
2
3
<button class="button normal-button">Normal</button>
<button class="button warning-button">Warning</button>
<button class="button error-button">Error</button>

这种方法虽然不是正统意义上的继承,但是同样可以达到避免冗余代码的效果。

CSS Module中的继承

CSS Module并不是CSS预处理器,而是一种CSS模块化的方式,它允许我们将CSS样式封装在模块中,并且可以通过导入的方式来使用其他模块的样式。CSS Module的文件名都是以.module.css结尾,比如users.module.css

在CSS Module中,可以使用composes关键字来实现样式的继承(扩展)。假设我们有一个基础的按钮样式button.module.css,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* button.module.css */
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

.normal-button {
composes: button; /* 继承button样式 */
background-color: #4caf50; /* Green */
}

.warning-button {
composes: button; /* 继承button样式 */
background-color: #ff9800; /* Orange */
}

.error-button {
composes: button; /* 继承button样式 */
background-color: #f 44336; /* Red */
}

使用的时候,就无需在指定.button类了,直接使用各自的颜色类即可:

1
2
3
<button class="normal-button">Normal</button>
<button class="warning-button">Warning</button>
<button class="error-button">Error</button>

SaSS中的继承

SaSS(Syntactically Awesome Style Sheets)是一个CSS预处理器,它提供了许多强大的功能,包括变量、嵌套、混合宏(mixins)和继承等。SaSS中的继承可以通过@extend指令来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* base.scss */
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

/* normal.scss */
.normal-button {
@extend .button; /* 继承button样式 */
background-color: #4caf50; /* Green */
}

/* warning.scss */
.warning-button {
@extend .button; /* 继承button样式 */
background-color: #ff9800; /* Orange */
}

/* error.scss */
.error-button {
@extend .button; /* 继承button样式 */
background-color: #f44336; /* Red */
}

使用方法也是一样的:

1
2
3
<button class="normal-button">Normal</button>
<button class="warning-button">Warning</button>
<button class="error-button">Error</button>

Less中的继承

Less是另一个流行的CSS预处理器,它也提供了类似于SaSS的继承功能。Less中的继承可以通过&:extend语法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* base.less */
.button {
width: 100px;
height: 50px;
border-radius: 8px;
border: 3px solid white;
}

/* normal.less */
.normal-button {
&:extend(.button); /* 继承button样式 */
background-color: #4caf50; /* Green */
}

/* warning.less */
.warning-button {
&:extend(.button); /* 继承button样式 */
background-color: #ff9800; /* Orange */
}

/* error.less */
.error-button {
&:extend(.button); /* 继承button样式 */
background-color: #f44336; /* Red */
}

使用方法同上。

好了,今天就到这里,祝大家编程愉快,我们明天再见,觉得有用,就点个关注吧!

介绍

今天来点简单的,如何在html文件中引入并使用javascript文件,这看起来是一个非常简单的问题,但是对于习惯了使用前端框架的我们来说,其实并不简单,如果大家不查资料,仅凭记忆来写,可能很多人都写不出来。

我们先从简单的开始,先回顾一下如何在HTML中使用普通的javascript文件。

在HTML中使用普通的javascript文件

编写javascript文件

首先我们编写一个简单的javascript文件math.js,文件内容很简单,只包含一个加法函数add,代码如下:

1
2
3
4
// math.js
function add(a, b) {
return a + b;
}

在html文件中引入javascript文件

在同一目录下新建一个文件index.html,然后引入前面定义的javascript文件,并调用add函数,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en-US">
<head>
<script src="math.js"></script>
</head>
<body>
<div id="result"></div>
<script>
const sum = add(1, 2);
document.getElementById("result").innerText = "Sum: " + sum;
</script>
</body>
</html>

解释一下上面的代码:

  1. <head>标签中使用<script src="math.js"></script>引入math.js文件,这样就可以在后续的脚本中使用math.js中定义的函数了。
  2. <body>标签中,我们有一个<div>元素用于显示结果,id为result
  3. <body>标签的最后,我们有一个内联的<script>标签,在这里我们调用了add(1, 2)函数,并将结果显示在result元素中。

用浏览器打开这个html文件,页面会显示Sum: 3
到现在为止,一切正常,现在我们更进一步,采用ES6的模块化语法来重写这个例子。

在HTML中使用ES6模块

我们修改math.js文件,使用ES6的export语法来导出add函数,代码如下:

1
2
3
4
// math.js
export function add(a, b) {
return a + b;
}

此时刷新浏览器,你应该会看到如下错误信息:

1
Uncaught SyntaxError: Unexpected token 'export'

为什么呢?从报错信息来看,浏览器并不认识export这个token,这说明浏览器无法识别ES6模块化语法。
所以我们需要告诉浏览器这是一个模块化的脚本,方法是给<head>中的<script>标签添加type="module"属性,代码如下:

1
2
3
<head>
<script type='module' src="math.js"></script>
</head>

再次刷新浏览器,你会发现还是不行,报错信息变成了:

1
Uncaught ReferenceError: add is not defined

从报错信息来看,浏览器找不到add函数,原因是对于ES6模块导出的方法,我们是不能直接调用的,必须使用import语法来导入,修改index.html文件如下:

1
2
3
4
5
6
7
8
<body>
<div id="result"></div>
<script>
import { add } from "./math.js"; /* 导入add函数 */
const sum = add(1, 2);
document.getElementById("result").innerText = "Sum: " + sum;
</script>
</body>

这次应该没啥问题了吧,再次刷新浏览器,发现还是报错:

1
Uncaught SyntaxError: Cannot use import statement outside a module

为什么呢?
这是因为我们在body中的<script>标签中使用了import语法,但是这个<script>标签并没有指定type="module"属性,所以浏览器认为这是一个普通的脚本,而不是一个模块化的脚本。普通脚本是无法导入ES6模块的。

继续修改index.html文件,给body中的<script>标签添加type="module"属性,代码如下:

1
2
3
4
5
<script type="module">
import { add } from "./math.js";
const sum = add(1, 2);
document.getElementById("result").innerText = "Sum: " + sum;
</script>

再次刷新浏览器,终于不报错了。

怎么样,是不是没有那么简单?我们来总结一下吧:

总结

HTML中使用普通的javascript脚本

  1. 定义普通的javascript文件,直接编写函数。
  2. 在html文件中引入这个javascript文件时,使用<script src="..."></script>标签引入。
  3. 在html文件中调用这个函数时,直接使用函数名调用即可。

HTML中使用ES6模块化脚本

  1. 首先定义ES6模块化的javascript文件,使用export语法导出函数。
  2. 在html文件中引入这个javascript文件时,必须给<script>标签添加type="module"属性。
  3. 在html文件中调用这个模块化的函数时,必须使用import语法导入,并且也要给<script>标签添加type="module"属性。

祝大家编程愉快!觉得有用就点个关注吧!

介绍

今天来点轻松的话题,如何给一个html元素添加tooltip,也就是鼠标悬停时显示一个提示信息。实现tooltip的方式有很多,我们逐一介绍一下。

方式一:使用html的title属性

最简单的方式就是使用html元素的title属性,给一个html元素添加title属性后,鼠标悬停时,浏览器会自动显示title属性的内容作为tooltip。

以下代码为div元素添加了一个tooltip,内容为”This is a tooltip”,当鼠标悬停在div元素上时,会显示这个tooltip。

1
<div title="This is a tooltip">Hover on me</div>
  • 优点:实现简单,不需要额外的css或js代码,浏览器会自动处理tooltip的显示和隐藏。
  • 缺点:tooltip的样式和位置无法自定义,完全依赖浏览器的默认行为。触发速度慢。

方式二: 使用CSS实现tooltip

首先定义元素的HTML结构,以下代码中,外层的div元素作为tooltip的触发区域,内层的span元素作为tooltip的内容。

1
2
3
4
<div class="tooltip">
Hover over me
<span class="tooltip-text">This is a custom tooltip</span>
</div>

然后使用CSS来实现tooltip,代码中有详细的注释,不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
.tooltip {
/*采用相对定位将外层div元素放置在 100px, 100px处,防止tooltip超出窗口*/
position: relative;
top: 100px;
left: 100px;
cursor: pointer; /* 鼠标悬停时显示为手型 */
}

/* tooltip的样式 */
.tooltip .tooltip-text {
visibility: hidden; /* 默认隐藏,hover时才显示 */
width: 120px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
}

/* 这里用来绘制tooltip下面的小三角形 */
.tooltip .tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}

/* hover时显示tooltip */
.tooltip:hover .tooltip-text {
visibility: visible;
}
  • 优点:可以自定义tooltip的样式和位置,灵活性更高。触发速度快。
  • 缺点:需要编写额外的css代码,稍微复杂一些。

方式三:使用JavaScript实现tooltip

这是最灵活的方式了,可以完全自定义tooltip的行为和样式。以下代码中,我们使用JavaScript来动态创建和显示tooltip。

1
2
<div class="js-tooltip">How on me</div>
<div class="tooltip-container hidden">This is a JavaScript tooltip</div>

用js控制tooltip的显示和隐藏:

1
2
3
4
5
6
7
8
9
10
const div = document.querySelector(".js-tooltip");
const tooltip = document.querySelector(".tooltip-container");

div.addEventListener("mouseenter", () => {
tooltip.style.display = "block";
});

div.addEventListener("mouseleave", () => {
tooltip.style.display = "none";
});

用css定义tooltip的样式,这里写得比较简单,大家可以根据需要随意添加。

1
2
3
4
5
6
7
8
9
10
11
.js-tooltip {
margin-top: 32px;
}

.tooltip-container {
position: absolute;
background: #333;
color: white;
padding: 10px;
border-radius: 5px;
}

总结

请以markdown表格的形式比较上述三种方式的优缺点,请添加一列显示速度。

方式 优点 缺点 显示速度
使用title属性 实现简单,不需要额外的css或js代码,浏览器会自动处理tooltip的显示和隐藏。 tooltip的样式和位置无法自定义,完全依赖浏览器的默认行为。
使用CSS实现tooltip 可以自定义tooltip的样式和位置,灵活性更高。 需要编写额外的css代码,稍微复杂一些。
使用JavaScript实现tooltip 最灵活的方式,可以完全自定义tooltip的行为和样式。 需要编写额外的js代码,稍微复杂一些。

今天就到这里了,祝大家编程愉快,我们明天再见,觉得有用,就点个关注吧!

码字不易,在整个互联网充斥着大量AI生成的垃圾内容的今天,还在坚持纯古法手写的人不多, 我Philip是其中一个。

各位老铁大家好,今天我们继续学习Ant Design中的Table组件,修改Table中的内容是一个常见的需求,今天我们来看看如何实现Table中的行编辑功能。我们会在每一行的末尾添加两个按钮,分别是编辑和取消,默认情况下,行数据是只读的,点击编辑按钮时,行数据会变成可编辑模式,点击取消则会恢复为只读模式。