Web前端开发(6) JS最佳实践
2023-08-09 14:53:19 # NJU # Web前端开发

8-JavaScript最佳实践

1. 最佳实践

避免全局变量

  • 请尽量少地使用全局变量
  • 它包括所有的数据类型、对象和函数
  • 全局变量和函数可被其他脚本覆盖
  • 请使用局部变量替代, 并学习如何使用闭包

始终声明局部变量

  • 所有在函数中使用的变量应该被声明为局部变量
  • 局部变量必须通过 var 关键词来声明, 否则它们将变成全局变量
  • 严格模式不允许未声明的变量
    • "use strict";

为什么使用严格模式

  • 消除Javascript语法的一些不合理、不严谨之处, 减少一些怪异行为
  • 消除代码运行的一些不安全之处, 保证代码运行的安全
  • 提高编译器效率, 增加运行速度
  • 为未来新版本的Javascript做好铺垫。

严格模式

  • ES6的模块自动采用严格模式, 不管模块头部有没有use strict
  • 严格模式有以下限制
    • 变量必须声明后再使用
    • 函数的参数不能有同名属性, 否则报错
    • 不能使用with语句
    • 不能对只读属性赋值, 否则报错
    • 不能使用前缀0表示⼋进制数, 否则报错
    • 不能删除不可删除的属性, 否则报错
    • 不能使用delete prop删除变量, 会报错, 只能删除属性delete global[prop]
    • eval不会在它的外层作用域引入变量
    • eval和arguments不能被重新赋值
    • arguments不会自动反映函数参数的变化
    • 不能使用arguments.callee
    • 不能使用arguments.caller
    • 禁止this指向全局对象
    • 不能使用 fn.callerfn.arguments 获取函数调用的堆栈
    • 增加了保留字(比如protected, static 和 interface)

在顶部声明

  • 一项好的编码习惯是把所有声明放在每段脚本或函数的顶部。
  • 这么做的好处是:
    • 获得更整洁的代码
    • 提供了查找局部变量的好位置
    • 更容易避免不需要的全局变量
    • 减少不需要的重新声明的可能性

初始化变量

  • 在您声明变量时对其进行初始化是个好习惯
  • 这么做的好处是:
    • 更整洁的代码
    • 在单独的位置来初始化变量
    • 避免未定义值

请不要声明数值、字符串或布尔对象

  • 请始终将数值、字符串或布尔值视作原始值。而非对象。
  • 如果把这些类型声明为对象, 会拖慢执行速度, 并产生讨厌的副作用
1
2
3
4
5
6
7
8
9
// Example
var x = "John";
var y = new String("John");
(x === y) // is false because x is a string and y is an object.

//Or even worse:
var x = new String("John");
var y = new String("John");
(x == y) // is false because you cannot compare objects.

请勿使用 new Object()

  • 请使用 {} 来代替 new Object()
  • 请使用 "" 来代替 new String()
  • 请使用 0 来代替 new Number()
  • 请使用 false 来代替 new Boolean()
  • 请使用 [] 来代替 new Array()
  • 请使用 /()/ 来代替 new RegExp()
  • 请使用 function (){} 来代替 new Function()

意识到自动类型转换

  • 请意识到数值会被意外转换为字符串或 NaN(Not a Number)
  • JavaScript 属于松散类型。变量可包含不同的数据类型, 并且变量能够改变其数据类型
1
2
3
4
5
6
7
8
9
10
11
// Example
var x = "Hello"; // typeof x is a string
x = 5; // changes typeof x to a number
// When doing mathematical operations, JavaScript can convert numbers to strings, Example:
var x = 5 + 7; // x.valueOf() is 12, typeof x is a number
var x = 5 + "7"; // x.valueOf() is 57, typeof x is a string
var x = 5 - 7; // x.valueOf() is -2, typeof x is a number
var x = 5 - "7"; // x.valueOf() is -2, typeof x is a number
var x = 5 - "x"; // x.valueOf() is NaN, typeof x is a number
// Subtracting a string from a string, does not generate an error but returns NaN (Not a Number), Example
"Hello" - "Dolly" // returns NaN

使用 === 比较

  • == 比较运算符总是在比较之前进行类型转换(以匹配类型)
  • === 运算符会强制对值和类型进行比较
1
2
3
4
5
6
7
8
//Example
0 == ""; // true
1 == "1"; // true
1 == true; // true

0 === ""; // false
1 === "1"; // false
1 === true; // false

使用 Parameter Defaults

  • 如果调用函数时缺少一个参数, 那么这个缺失参数的值会被设置为 undefined
  • undefined 值会破坏您的代码。为参数设置默认值是一个好习惯
1
2
3
4
5
function myFunction(x, y) {
if (y === undefined) {
y = 0;
}
}

用 default 来结束 switch

1
2
3
4
5
6
7
8
9
10
11
switch (new Date().getDay()) {
case 0:
day = "Sunday";
break;
...
case 6:
day = "Saturday";
break;
default:
day = "Unknown";
}

避免使用 eval()

  • eval() 函数用于将文本作为代码来允许。在几乎所有情况下, 都没有必要使用它
  • 因为允许任意代码运行, 它同时也意味着安全问题

“For in” Statements

  • 遍历对象内的成员时, 你也会得到方法函数。为了解决这个问题, 应始终将你的代码包装在一个 if 语句中来过滤信息
1
2
3
4
5
for(key in object) {
if(object.hasOwnProperty(key) {
...then do something...
}
}

减少循环中的活动

  • 循环每迭代一次, 循环中的每条语句, 包括 for 语句, 都会被执行
  • 能够放在循环之外的语句或赋值会使循环运行得更快
1
2
3
4
5
6
7
// 差的代码:
var i;
for (i = 0; i < arr.length; i++) {}
// 更好的代码:
var i;
var l = arr.length;
for (i = 0; i < l; i++) {}

2. Vanilla JS

What is Vanilla JS?

  • vanillaJS 是史上最轻量跨平台前端框架, 没有之一
  • vanillaJS 小巧而符合直觉, Bootstrap5 舍弃了 JQuery , 使用的知名企业很多
  • 其实就是原生 js

模块化

  • VanillaJS包括如下模块, 下载安装时可以只选择需要的模块, 以便提高性能。
    • 核心功能、DOM(遍历/选择器)、基于原型的对象系统、AJAX、动画、事件系统、正则表达式、函数作为第一类对象、闭包、数学库、数组库、字符串库

如何使用

  • 引入方式只需要在html中加入这行script:
    • <script src="path/to/vanilla.js"></script>
  • 当部署在正式环境中, 不需要引用

  • plain JavaScript without any additional libraries

通过ID获取元素

image-20230125155615189

JS框架的优点

  • JS框架封装了复杂困难的代码
  • JS框架能加快开发速度, 更快完成项目
  • JS框架让你更专注于产品内容的价值, 而不是实现过程
  • JS框架让合作更简单, 大家都对基础代码有共同的理解
  • JS框架还会强迫你练习, 多实践, 顺能生巧

JS框架的问题

  • 每个项目的开发都会遇到框架文档没有说明的问题, 这时候就要深入框架查找原因, 这时候就需要对原生JavaScript的深度掌握
  • 新框架频繁发布, 更新快速, 一旦确定了项目的技术栈, 随着时间, 如何升级更新是个问题

Why Vanilla JavaScript

  • 学会 Vanilla JavaScript能真正理解JS框架, 甚至能为其贡献代码, 还能帮助选择合适的框架
  • 如果不知道Web基本原则, 语言本身的演变和新框架的不断到来。。。
  • 知道纯JS将成为一个能够(不用疯狂搜索原因)解决复杂问题的关键工程师
  • 增加通用能力和生产力, 不管在前端还是后端
  • 创新的工具, 而不只是执行
  • 指导什么时候使用或者不使用框架
  • 更好地了解浏览器和计算机工作原理

3. 正则表达式

什么是正则表达式?

  • 正则表达式是用于匹配字符串中字符组合的模式
  • 正则表达式是构成搜索模式(search pattern)的字符序列
  • 当您搜索文本中的数据时, 您可使用搜索模式来描述您搜索的内容
  • 正则表达式可以是单字符, 或者更复杂的模式
  • 正则表达式可用于执行所有类型的文本搜索和文本替换操作
1
2
3
4
// 格式形如: /正则表达式主体/修饰符(可选)
// 邮箱
/([\w\-]+\@[\w\-]+\.[\w\-]+)/
var re = /ab+c/;

使用简单模式

  • 以 / 开始和结束

  • 最简单的正则是子字符串匹配

  • 下述正则表达式匹配任一包含 “abc” 的字符串:

    • YES: “abc”, “abcdef”, “defabc”, “.=.abc.=.”, …
    • NO: “fedcba”, “ab c”, “PHP”, …

    • /abc/

匹配单个字符: .

  • . 匹配单个字符, 除了换行和行结束符
    • /.oo.y/ 匹配 “Doocy”, “goofy”, “LooNy”, …
  • 修饰符 i 执行对大小写不敏感的匹配
    • /mart/i matches “Marty Stepp”, “smart fellow”, “WALMART”, …

特殊字符: |, (), ^, \

  • | 选择匹配
    • /abc|def|g/ 匹配 “abc”, “def”, or “g”
  • () 子表达式:把一个表达式分割为几个子表达式是非常有用的
    • /(Homer|Marge) Simpson/ 匹配 “Homer Simpson” or “Marge Simpson”
  • ^ 匹配输入的开始。如果多行标志被设置为 true, 那么也匹配换行符后紧跟的位置
    • /^A/ 并不会匹配 “an A” 中的 ‘A’, 但是会匹配 “An E” 中的 ‘A’
  • \$ 匹配输入的结束。如果多行标志被设置为 true, 那么也匹配换行符前的位置
    • 例如, /t$/ 并不会匹配 “eater” 中的 ‘t’, 但是会匹配 “eat” 中的 ‘t’
  • 转义
    • 如果要匹配特殊字符: /\\$.[]()^*+?
    • /<br \/>/ 匹配 <br /> 标记

量词: *, +, ?

  • * 匹配 0 次或更多次
    • /abc*/ 匹配 “ab”, “abc”, “abcc”, “abccc”, ..
    • /a(bc)*/ 匹配 “a”, “abc”, “abcbc”, “abcbcbc”, …
    • /a.*a/ 匹配 “aa”, “aba”, “a8qa”, “a!?_a”, …
  • + 匹配 1 次或更多次
    • /a(bc)+/ 匹配 “abc”, “abcbc”, “abcbcbc”, …
    • /Goo+gle/ 匹配 “Google”, “Gooogle”, “Goooogle”, …
  • ? 匹配 0 或 1 次
    • /a(bc)?/ 匹配 “a” or “abc”

更多量词: {min,max}

  • {min,max}, 允许重复的次数, 其中, “min” 是 0 或一个正整数, “max” 是一个正整数, 而 max > min, 至少匹配 “min” 次, 最多匹配 “max” 次
    • /a(bc){2,4}/ matches “abcbc”, “abcbcbc”, or “abcbcbcbc”
  • 省略形式
    • {2,} 匹配至少 2 次
    • {,6} 匹配最多 6 次
    • {3} 匹配正好 3 次

字符集: []

  • [] 字符集。 匹配任何一个包含的字符。
    • /[bcd]art/ 匹配包含 “bart”, “cart”, and “dart” 的字符串
    • 等同于 “/(b|c|d)art/“ 但更短
    • 不需要转义
  • []中, 许多修饰符作为正常字符
    • /what[!*?]*/ matches “what”, “what!”, “what?**!”, “what??!”, …
  • 匹配 DNA (strings of A, C, G, or T) 的正则表达式?

字符范围: [start-end]

  • 可以使用连字符来指定字符范围, 但如果连字符显示为方括号中的第一个或最后一个字符, 则它将被视为作为普通字符包含在字符集中的文字连字符。也可以在字符集中包含字符类。
    • /[a-z]/ 匹配任一小写字母
    • /[a-zA-Z0-9]/ 匹配任一大小写字母和数字
  • 起始 ^ 表示一个否定的或被补充的字符集。也就是说, 它匹配任何没有包含在括号中的字符
    • /[^abcd]/ 匹配除了 a, b, c, or d 以外的字母
  • 字符集中, - 需要转义
    • /[+\-]?[0-9]+/ 匹配 + 或 -, 随后至少一位数字
  • 匹配评估分数 A, B+, or D- 的正则表达式?
    • /[ABCDF][+\-]?/

字符

  • 特殊字符集:
    • \d 匹配任何数字(阿拉伯数字)。 相当于 [0-9]
    • \D 匹配任何非数字(阿拉伯数字)的字符。相当于[^0-9]
    • \w 匹配基本拉丁字母中的任何字母数字字符, 包括下划线。相当于 [A-Zaz0-9_]
    • \W 匹配任何不是来自基本拉丁字母的单词字符。相当于 [^A-Zaz0-9_]
    • \s 匹配一个空白字符, 包括空格、制表符、换页符和换行符
    • \S 匹配一个非空白字符
  • 至少$1000.00的正则表达式?
    • /\$[1-9]\d{3,}\.\d{2}/

JavaScript 正则表达式的使用

  • 在 JavaScript 中, 正则表达式常用于两个字符串方法:search() 和 replace()
1
2
var str = "Visit W3School";
var n = str.search(/w3school/i);
  • 在 JavaScript 中, RegExp 对象是带有预定义属性和方法的正则表达式对象
  • 使用 test(), test() 方法是一个正则表达式方法
1
2
var patt = /e/;
patt.test("The best things in life are free!"); //true
  • 使用 exec(), 用于检索字符串中的正则表达式的匹配。该函数返回一个数组, 其中存放匹配的结果。如果未找到匹配, 则返回值为 null
1
/e/.exec("The best things in life are free"); //e

JavaScript RegExp 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var patt = new RegExp(pattern,modifiers);
//或者更简单的方式:
var patt = /pattern/modifiers;

//等价
var re = new RegExp("\\w+");
var re = /\w+/;

var re = /\\/gm;
//正则构造函数
var reg = new RegExp("\\\\","gm");
var foo = "abc\\123";  //foo的值为"abc\123"
console.log(re.test(foo)); //true
console.log(reg.test(foo));

4. 函数

编写更好的函数

  • Don’t Repeat Yourself (DRY) 不重复造轮子: 封装为函数, 对象模块
  • Do One Thing (DOT) 一次只做一件事情:提升代码复用性, 易读性与可调式性
  • Keep It Simple Stupid (KISS) 保持简单:技巧, 高深 晦涩, 一行代码中安排多个原子性任务
  • Less Is More 少即是多:易读, 避免一次执行多个任务, 函数内容尽可能精简, 不贪多, 代码量独立完成一个功能点, 拆分多个子函数

函数定义

arguments对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Warning: arguments.callee is deprecated. 
Use with caution.
Used here strictly for illustration.
*/
function foo() {
return arguments.callee;
}
foo(); // => [Function: foo]

//wrong, 不能在分支语句中声明函数, 使用函数表达式
var score = 6;
if(score>5){
function grade() {
return 'pass';
}
} else {
function grade() {
return 'fail';
}
}

函数表达式

  • 函数表达式的优点是可以像给变量赋值一样将函数赋给变量
  • 可以依靠函数表达式可靠地遵循应用程序逻辑。例如, 条件分支中按预期工作
  • 缺点是函数表达式创建匿名函数, 除非显式提供名称
1
2
3
4
var bar = function () {
return arguments.callee;
};
bar(); // => [Function] (Note: It's anonymous.)

对象字面量(方法字面量)

1
2
3
4
5
6
var baz={
f: function () {
return arguments.callee;
}
};
baz.f(); // => [Function] (Note: Also anonymous.)

优点

  • 使用对象字面量对相关函数进行分组非常容易。代码更有条理, 可读性更强。上下文中的代码更容易理解和维护
  • 容易拆解和排列:如果模块变得太大, 可以更容易地重新排列代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var lightBulbAPI = {
toggle: function () {},
getState: function () {},
off: function () {},
on: function () {},
blink: function () {}
};

var lightbulbAPI = {
toggle: function toggle() {},
getState: function getState() {},
off: function off() {},
on: function on() {},
blink: function blink() {}
};

5. 原型

可参考 https://zh.javascript.info/prototype-inheritance

Prototypes

  • 原型对象为所有对象实例所共享, 因此这些实例也共享了原型函数的成员。通过内部属性绑定到原型
1
2
3
4
5
var book = {
title: "High Performance JavaScript",
publisher: "Yahoo! Press"
};
alert(book.toString()); //"[object Object]"

image-20230125214619517

例子—原型链

  • 实例的成员和原型的成员是一种链状的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
function Book(title, publisher) {
this.title = title;
this.publisher = publisher;
}
Book.prototype.sayTitle = function() {
alert(this.title);
};
var book1 = new Book("High Performance JavaScript", "Yahoo! Press");
var book2 = new Book("JavaScript: The Good Parts", "Yahoo! Press");
alert(book1 instanceof Book); //true
alert(book1 instanceof Object); //true
book1.sayTitle(); //"High Performance JavaScript"
alert(book1.toString()); //"[object Object]"

image-20230125214941083

Class

  • 一种语法糖, 对象的一个模板, 本质是一个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义类
class Point {
// 注意函数构造的方式
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p1 = new Point(5, 5);
p1.toString(); //"(5, 5)"
typeof Point // function
p1.constructor == Point //true

函数声明可以提升

函数表达式、class 不可以提升, 需要先声明再使用

extends

1
2
3
4
5
6
7
8
9
10
11
12
class Square extends Point{
constructor(x){
super(x, x);
}
toString(){
return super.toString() + 'Square!';
}
}
var s1 = new Square(4);
s1.toString(); //"(4, 4)Square!"
s1 instanceof Point // true
s1 instanceof Square // true

__proto__

1
2
Square.__proto__ === Point // true, 子类的原型指向父类
Square.prototype.__proto__ === Point.prototype // true

super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
constructor(a){
this.x = a;
}
}
A.prototype.y = 2;

class B extends A{
constructor(a){
super();
}
getY(){
super() // 报错
return super.y
}
}

原生构造函数的继承

1
2
3
4
5
6
7
8
9
10
11
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

static

1
2
3
4
5
6
7
8
9
10
11
class A{
static add(x, y){
return x + y;
}
}
A.add(1, 2);
var a = new A();
a.add()// error

class B extends A{}
B.add(2, 2)// 4

6. this

this

  • 在函数体中, 非显式或隐式地简单调用函数时, 在严格模式下, 函数内的 this 会被绑定到 undefined 上, 在非严格模式下则会被绑定到全局对象 window/global 上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function f1() {
    console.log(this);
    }
    function f2() {
    "use strict";
    console.log(this);
    }
    f1(); // window
    f2(); // undefined
    • this指向最后调用它的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var foo = {
    bar: 10,
    fn: function() {
    console.log(this)
    console.log(this.bar)
    }
    }
    foo.fn(); // {bar: 10, fn: f}, 10
    var fn1 = foo.fn;
    fn1(); // window, undefined
  • 一般使用 new 方法调用构造函数时, 构造函数内的 this 会被绑定到新创建的对象上

    1
    2
    3
    4
    5
    function Foo() {
    this.bar = "Lucas"
    }
    const instance = new Foo()
    console.log(instance.bar) // Lucas
  • 一般通过 call/apply/bind 方法显示调用函数时, 函数体内的 this 会被绑定到指定参数的对象上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const foo = {
    name: 'lucas',
    logName: function() {
    console.log(this.name)
    }
    }
    const bar = {
    name: 'mike'
    }
    console.log(foo.logName.call(bar)) // mike
  • 一般通过上下文对象调用函数时, 函数体内的this会被绑定到该对象上

    1
    2
    3
    4
    5
    6
    7
    var student = {
    name: 'Lucas',
    fn: function() {
    return this
    }
    }
    console.log(student.fn() === student) // true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var person = {
    name: 'Lucas',
    brother: {
    name: 'Mike',
    fn: function() {
    return this.name
    }
    }
    }
    console.log(person.brother.fn()) // Mike
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var o1 = {
    text: 'o1',
    fn: function() {
    return this.text
    }
    }
    var o2 = {
    text: 'o2',
    fn: function() {
    return o1.fn()
    }
    }
    var o3 = {
    text: 'o3',
    fn: function() {
    var fn = o1.fn
    return fn()
    }
    }
    console.log(o1.fn()) // o1
    console.log(o2.fn()) // o1
    console.log(o3.fn()) // undifined
  • 在箭头函数中, this的指向是由外层(函数或全局)作用域来决定的

    • 由定义的作用域决定而非调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const foo = {
    fn: function(){
    setTimeout(function() {
    console.log(this)
    })
    }
    }
    console.log(foo.fn()) // window

    var foo = {
    fn: function(){
    setTimeout(() => {
    console.log(this)
    })
    }
    }
    console.log(foo.fn()) // {fn:f}

this优先级

  • 显式绑定:通过call、apply、bind、new
  • 隐式绑定:根据调用关系确定this指向
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(a) {
console.log(this.a)
}
var obj1 = {
a: 1,
foo: foo
}
var obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 1
1
2
3
4
5
6
7
8
9
10
function foo(a) {
this.a = a
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2

var baz = new bar(3)
console.log(baz.a) // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
return a => {
console.log(this.a)
};
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1)
console.log(bar.call(obj2))
// 2
// undefined, 箭头函数的绑定无法被修改

7. Babel

Babel

  • Babel 是一个 JavaScript 编译器
  • Babel 是一个工具链, 主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法, 以便能够运行在当前和旧版本的浏览器或其他环境中
  • 下面列出的是 Babel 能为你做的事情:
    • 语法转换
    • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
    • 源码转换 (codemods)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const sum = (a,b)=>a+b
// 转换为
"use strict";
var sum = function sum(a, b) {
return a + b;
};

for(let i=0;i<5;i++){} console.log(i);
for(var i=0;i<5;i++){} console.log(i);
// 转换为
"use strict";
for (var _i = 0; _i < 5; _i++) {}
console.log(i);//undefined
for (var i = 0; i < 5; i++) {}
console.log(i);//5