文章目录
JavaScript 包含了 ECMAScript、DOM、BOM。
DOM (Document Object Model)
DOM 浏览器特有。
- 定义: DOM 是一种编程接口,用于访问和操作网页的内容和结构。它将网页的内容表示为一个树状结构,使得 JavaScript 可以动态地访问和更新网页内容、结构和样式。
- 主要功能:
- 节点: 通过 DOM,网页的每个部分(如元素、属性和文本)都可以视为一个节点。
- 节点操作: JavaScript 可以创建、修改、删除和替换这些节点。
- 事件处理: 通过 DOM,可以为网页中的事件(如点击、输入、加载等)添加事件监听器。
BOM (Browser Object Model)
- 定义: BOM 是用于与浏览器窗口及其功能进行交互的一组对象和方法。它允许 JavaScript 访问和操作浏览器窗口、文档、历史记录、导航、位置等。
- 主要功能:
window
对象: 代表浏览器窗口,提供了访问浏览器属性和方法的能力,例如alert()
,confirm()
,prompt()
,setTimeout()
,clearTimeout()
等。navigator
对象: 提供关于浏览器的信息,如浏览器版本和用户代理。screen
对象: 提供关于显示屏的分辨率和尺寸的信息。history
对象: 允许你访问和操作浏览器的历史记录。location
对象: 允许你访问和修改浏览器的 URL。
ECMA Script 6
let 和 const 命令
{
let a = 10; //let声明只在其所在的代码块有效
var b = 1;
}
a
b
//run:
a
^
ReferenceError: a is not defined
for(let i = 0; i < 10; i++){} //for循环中的局部变量 let i
console.log(i)
//run:
console.log(i)
^
ReferenceError: i is not defined
var a = []
for (var i = 0; i < 10; i++) { //var i 全局变量
a[i] = function() {
console.log(i);
}
}
a[6](); //10
var a = []
for (let i = 0; i < 10; i++) { //let i 全局变量
a[i] = function() {
console.log(i);
}
}
a[6](); //6
变量提升:
- var 存在变量提升。
- let 不存在。
console.log(a); //变量一定要在声明后使用
let a = 2;
//run:
console.log(a);
^
ReferenceError: Cannot access 'a' before initialization
console.log(a);
var a = 2;
//run:
undefined
暂时性死区:
var tmp = 123;
if (true) {
tmp = "abc";
let tmp; //let const 封闭作用域
}
//run:
tmp = "abc";
^
ReferenceError: Cannot access 'tmp' before initialization
if (true){
//暂时性死区 TDZ 开始
tmp = 'abc';
console.log(tmp);
let tmp; //TDZ temporal dead zone 结束
console.log(tmp); //undefined
tmp = 123;
console.log(tmp); //123
}
//run:
D:\jsPro\work\test.js:3
tmp = 'abc';
^
ReferenceError: Cannot access 'tmp' before initialization
function bar(x = y, y = 2) { //(x = 2, y = x)
return [x,y];
}
bar();
//run:
function bar(x = y, y = 2) {
^
ReferenceError: Cannot access 'y' before initialization
变量不能重复声明:
function() {
let a = 10;
var a = ; //error
}
function() {
let a = 10;
let a = 1; //error
}
function func(arg) {
let arg; //error
}
function func(arg) {
{ //局部作用域
let arg; //correct
}
}
块级作用域:
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n) //5
}
{
let a = 'secret';
function f() {
console.log("inner")
return a;
}
}
f()
//run:
PS D:\jsPro\work> node test.js
inner
//块级作用域外可以调用块级作用域内的函数,ES6 书里面写的这里会报错
let f;
{
let a = 'secret';
f = function () {
return a;
}
}
console.log(f());
//run:
PS D:\jsPro\work> node test.js
secret
const
const PI = 3.14; //常量值不可改变,声明时必须初始化,只在声明的块级作用域以内有效,不存在变量提升,存在暂时性死区,只能在声明后使用。
const foo = {}; //foo是常量,指向的地址不变,但里面的数据可变,对象声明为常量需要小心。
foo.prop = 123;
const a = [];
a.push("Hello");
const a = [];
a.push("Hello");
a.length = 0;
a = ["Dave"];
//run:
a = ["Dave"];
^
TypeError: Assignment to constant variable.
var a = [];
var b = [];
b.push("hello");
b.push("world")
console.log(b)
a = b
console.log(a)
//run:
PS D:\jsPro\work> node test.js
[ 'hello', 'world' ]
[ 'hello', 'world' ]
Object.freeze方法
const foo = Object.freeze({});
foo.prop = 123; //不会添加新属性
console.log(foo)
//run:
PS D:\jsPro\work> node test.js
{}
//将一个对象彻底冻结
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, value) => {
if (typeof obj[key] === 'object') {
constantize(obj[key]);
}
})
}
Object.freeze()
只会冻结对象的第一层。如果对象的属性值也是对象(嵌套对象),这些嵌套对象不会被自动冻结。
var constantize = (obj) => {
Object.freeze(obj); // 冻结对象,防止对其进行任何修改
Object.keys(obj).forEach((key) => { // 遍历对象的所有键
if (typeof obj[key] === 'object') { // 如果值是对象
constantize(obj[key]); // 递归调用 constantize 函数
}
});
}
// 创建一个包含嵌套对象的示例对象
const myObject = {
name: 'John',
age: 30,
address: {
city: 'New York',
zip: '10001'
},
hobbies: ['reading', 'gaming']
};
// 调用 constantize 函数
constantize(myObject);
// 尝试修改对象(将会失败,因为对象已经被冻结)
myObject.name = 'Jane'; // 无效
myObject.address.city = 'Los Angeles'; // 无效
myObject.hobbies.push('swimming'); // 无效
console.log(myObject);
run:
myObject.hobbies.push('swimming'); // 无效
^
TypeError: Cannot add property 2, object is not extensible
这个错误说明 myObject.hobbies
数组已经被冻结,无法对其进行修改(如添加新元素)。
跨模块常量
export const A = 1;
export const B = 2;
export const C = 3;
全局对象的属性
全局对象是最顶层的对象。浏览器环境中指的是 window对象。node.js 中指 global 对象。
var 命令和 function 命令声明的全局变量依旧是全局对象的属性;let 命令、const 命令 和 class 命令声明的全局变量不属于全局对象的属性。
var a = 1;
//node.js 中可以写成 global.a 或者写成 this.a
window.a
var a = 1;
console.log(a) //1
this.a = 2
console.log(this.a, a) //2 1
window.a = 3
console.log(window.a, this.a, a) //ReferenceError: window is not defined
//nodejs 环境中没有 window 对象
run:
PS D:\jsPro\work> node test.js
1
2 1
D:\jsPro\work\test.js:7
window.a = 3
^
ReferenceError: window is not defined
变量的结构赋值
数组结构赋值
var [a, b, c] = [1, 2, 3];
console.log(a,b,c) //1, 2, 3
嵌套解构赋值:
let [foo, [[bar], baz]] = [1,[[2], 3]];
console.log(a,b,c) //1, 2, 3
let [ , , third] = [1,2,3]
console.log(third) //3
let [x, ,y] = [1,2,3];
console.log(x, y); //1 3
let [head, ...tail] = [1,2,3,4,5];
console.log(head, tail)
//run:
1 [ 2, 3, 4, 5 ]
let [x, y, ...z] = ['a'];
console.log(x, y, z)
//run:
a undefined []
不完全解构:
let [x, y] = [1,2,3];
console.log(x, y) //1 2
let [a, [b], d] = [1, [2,3],4];
console.log(a,b,d) //1 2 4
var [x, y, z] = new Set(['a', 'b', 'c']);
console.log(x, y, z); //a b c
function* fibs() {
var a = 0;
var b = 1;
while (true) {
yield a;
[a,b] = [b, a + b];
}
}
var [a, b, c, d, e, f, g] = fibs();
console.log(a, b, c, d, e, f, g) //斐波那契数列
function\* fibs()
:
- 定义了一个生成器函数
fibs
,它会生成斐波那契数列的数字。
yield a;
:
yield
关键字会暂停函数执行,并返回当前的a
值。每次调用next()
时,函数会从这里继续执行。
[a, b] = [b, a + b];
:
- 使用解构赋值更新
a
和b
的值。a
被更新为b
,b
被更新为a + b
,这是生成下一个斐波那契数所需的操作。
解构赋值默认值:
var [foo = true] = [];
console.log(foo)
"use strict";
var [x, y = 'b'] = ['a'];
console.log(x, y) //a b
var [m, n = 'b'] = ['a', undefined];
console.log(m, n) //a b
var [x = 1] = [undefined];
console.log(x) //x = 1
var [x = 1] = [null];
console.log(x) // x = null , null不严格等于undefined,默认值不会生效
function f() {
console.log('aaa');
}
let [x = f()] = [1]; //默认值是表达式,惰性求值,只有在用到的时候求值
相当于:
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0]
}
console.log(x)
function f() {
console.log("aaa");
}
var [x = f()] = [undefined]; //aaa
function f() {
return "aaa";
}
let arr = [1];
let x = arr[0] === undefined ? f() : arr[0];
console.log(x) //1
function f() {
return "aaa";
}
let arr = [undefined];
let x = arr[0] === undefined ? f() : arr[0];
console.log(x) //aaa
对象解构赋值
var {foo, bar} = {foo: "aaa", bar: "bbb"};
console.log(foo, bar); //aaa bbb
var {bar, foo} = {foo: "aaa", bar: "bbb"};
console.log(foo, bar); //aaa bbb 和次序无关,按属性名赋值
var {baz} = {foo: "aaa", bar: "bbb"};
baz //undefined
var {foo: baz} = {foo: "aaa", bar: "bbb"};
console.log(baz) // foo的属性值赋值给新变量 baz
let obj = { first: "hello", last: "world"};
let {first: f, last: l} = obj;
console.log(f, l) //hello world
var {foo: foo, bar: bar} = {foo: "aaa", bar: "bbb"}; //完整写法
var {foo, bar} = {foo: "aaa", bar: "bbb"}; //省略属性名写法
let foo;
({foo} = {foo: 1});
console.log(foo) //1
let baz;
({bar:baz} = {bar: 1})
console.log(baz) //1
嵌套结构的对象:
var obj = {
p: [
"hello",
{
y: "World"
}
]
};
var {p: [x, {y}]} = obj;
console.log(x, y)
{ } 和 [ ] :
对象使用 {}
来定义,表示键值对的集合。用于存储和组织数据,例如属性和方法。
const person = {
name: 'Alice',
age: 30,
address: {
city: 'Wonderland',
postalCode: '12345'
}
};
console.log(person.address.city); // 输出 'Wonderland'
数组使用 []
来定义,表示有序的元素集合。用于存储列表或序列的数据,例如索引元素的集合。
const numbers = [1, 2, 3, [4, 5, [6, 7]]];
console.log(numbers[3][2][1]); // 输出 7
var node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
var {loc: {start: {line}}} = node;
console.log(line) //1
console.log(loc) //loc是模式
console.log(start) //start是模式
let obj = {};
let arr = [];
({foo: obj.prop, bar: arr[0]} = {foo: 123, bar: true});
console.log(obj, arr); //{ prop: 123 } [ true ]
对象解构指定默认值:
var {x = 3} = {};
console.log(x) //3
var {x, y = 5} = {x: 1};
console.log(x, y); //1 5
var {message: msg = "Something went wrong"} = {};
console.log(msg) //默认值生效的条件是,对象的属性严格等于undefined
var {x = 3} = {x: undefined};
console.log(x) //3
var {x = 3} = {x: null};
console.log(x) //null
var {foo} = {bar: 'baz'};
console.log(foo) //undefined 解构是被就是undefined
var x;
{x} = {x: 1}; //报错,{}会解析成一个代码块
console.log(x)
var x;
({x} = {x:1}); //正确
console.log(x)
let {log, sin, cos} = Math; //对象的解构赋值可以方便地将现有的对象的方法赋值到多个变量。
字符串解构赋值
const [a, b, c, d, e] = "hello";
console.log(a,b,c,d,e) //h e l l o
let {length: len} = "hello";
console.log(len) //5
数值和布尔值的解构赋值
let {toString: s} = 123;
console.log(s === Number.prototype.toString) //true
let {toString: s} = true;
console.log(s === Boolean.prototype.toString) //true
数值和布尔值都有 toString 属性。
解构赋值时,等号右边的不是对象就会先转换成对象,在进行解构赋值。undefined 和 null 无法转换成对象,解构赋值会报错。
let {prop: x} = undefined; //TypeError
let {prop: y} = null; //TypeError
函数参数解构赋值
function add([x, y]) { //参数不是数组,而是数组经过解构的结果x和y
return x + y;
}
console.log(add([1,2]));
function ad(x, y) {
return x + y;
}
console.log(ad(1,2));
console.log([[1,2],[3,4]].map(([a,b]) => a + b)); //[ 3, 7 ]
函数参数解构赋值默认值:
function move({x = 0, y = 0} = {}) {
return [x, y];
}
console.log(move({x:3, y:8}));
console.log(move({x: 3}));
console.log(move({}))
console.log(move())
//run:
[ 3, 8 ]
[ 3, 0 ]
[ 0, 0 ]
[ 0, 0 ]
function move({x, y} = {x: 0, y: 0}) { //函数的默认值,区别于函数参数解构赋值的默认值
return [x, y];
}
console.log(move({x:3, y:8}));
console.log(move({x:3}));
console.log(move({}))
console.log(move())
//run:
[ 3, 8 ]
[ 3, undefined ]
[ undefined, undefined ]
[ 0, 0 ]
undefined 会触发函数参数的默认值。
console.log([1, undefined, 3].map((x = 'yes') => x));
//run:
[ 1, 'yes', 3 ]
圆括号的问题
//全部报错,变量声明语句,模式不能使用圆括号
var [(a)] = [1];
var {x: (c)} = {};
var {o: ({p: p})} = {o: {p: 2}};
函数参数中,模式不能带有圆括号:
//函数参数也属于变量声明,不能使用圆括号
function f([(z)]) { return z;}
//全部报错
({p: a}) = {p: 42};
([a]) = [5];
[({p: a}), {x:c}] = [{},{}];
可以使用圆括号的情况:
//赋值语句的非模式部分可以使用圆括号,以下都是赋值语句而不是声明语句
[(b)] = [3];
({p: (d)} = {});
[(parseInt.prop)] = [3];
解构赋值的用途
交换变量值:
[x, y] = [y, x];
从函数返回多个值:
function example() {
return [1,2,3];
}
var [a, b, c] = example();
console.log(a,b,c);
//返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
var {foo, bar} = example();
console.log(foo,bar);
函数参数的定义:
函数参数有序无序的情况下参数都能准确地一一对应:
function f([x, y, z]) {
console.log(`x:${x},y:${y},z:${z}.`)
}
f([1,2,3]) //x:1,y:2,z:3.
function f({x, y, z}) {
console.log(`x:${x},y:${y},z:${z}.`)
}
f({z:3, x:1, y:2}); //x:1,y:2,z:3.
提取JSON数据:
var jsonData = {
id: 42,
status: "ok",
data: [867, 5309]
}
let {id, status, data: number} = jsonData;
console.log(id, status, number); //42 ok [ 867, 5309 ]
函数参数的默认值:
jQuery.ajax = function (url, {
async = true,
beforeSend = function() {},
cache = true,
complete = function() {},
crossDomain = false,
global = true,
//... more config
}) {
// ... do stuff
};
函数参数的默认值避免了在函数体内部写 var foo = config.foo || 'default foo';
config.foo
: 这是一个可能存在的变量或对象属性。如果 config
对象中有一个 foo
属性,它的值会被使用。
||
(逻辑或运算符): 逻辑或运算符会检查 config.foo
的值。如果 config.foo
是假值(undefined
、null
、false
、0
、NaN
或空字符串 ""
),则 ||
运算符会返回其右侧的值。
var config = {
foo: undefined
}
var foo = config.foo || 'default foo';
console.log(foo) //default foo
var config = {
foo: null
}
var foo = config.foo || 'default foo';
console.log(foo) //default foo
var config = {
foo: 0
}
var foo = config.foo || 'default foo';
console.log(foo) //default foo
var config = {
foo: false
}
var foo = config.foo || 'default foo';
console.log(foo) //default foo
var config = {
foo: ""
}
var foo = config.foo || 'default foo';
console.log(foo) //default foo
var config = {
foo: NaN
}
var foo = config.foo || 'default foo';
console.log(foo) //default foo
//提供默认值: 当你希望确保一个变量总是有一个有效的值,即使原始值不存在或为假值时。
function greet(name) {
var displayName = name || 'Guest';
console.log('Hello, ' + displayName);
}
greet(); // 输出 'Hello, Guest'
greet('Alice'); // 输出 'Hello, Alice'
-
假值: 逻辑或运算符
||
只会在左侧值是“假值”时使用右侧的值。假值包括undefined
、null
、false
、0
、NaN
和空字符串""
。这意味着,如果config.foo
的值是这些假值中的一个,默认值将会被使用。 -
更严格的默认值: 如果想在
config.foo
为undefined
或null
时使用默认值,可以使用空值合并运算符??
(在 ES2020 中引入):??
运算符仅在左侧的值为undefined
或null
时使用右侧的值。var config = { foo: NaN } var foo = config.foo ?? 'default foo'; console.log(foo) //NaN
var config = { foo: 0 }; var foo = config.foo ?? 'default foo'; console.log(foo); // 输出 0
遍历 Map 解构:
任何部署了 Iterator 接口的对象可以使用 for … of 循环遍历。Map 原生支持 Iterator 接口。
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(`key ${key} is ${value}`);
}
只获取键:
for (let [key] of map) { ... ...}
只获取值:
for (let [value] of map) {... ...}
输入模块的指定的方法:
const { SourceMapConsumer, SourceNode } = require("source-map");
字符串的扩展
Unicode 表示方法
console.log("\u0061"); //a
console.log("\uD842\uDFB7"); //𠮷
console.log("\u20BB7"); //₻7
console.log("\u{20BB7}"); //𠮷
console.log("\u{41}\u{42}\u{43}"); //ABC
console.log("hell\u{6F}"); //hello
console.log("\u{1F680}"); //🚀
console.log("\uD83D\uDE80"); //🚀
console.log("\u{1F680}" === "\uD83D\uDE80") //true
javascript 6 中方法表示字符:
'z'
console.log('\z' === 'z'); //true
console.log('\172' === 'z'); //true
console.log('\x7A' === 'z'); //true
console.log('\u007A' === 'z'); //true
console.log('\u{7A}' === 'z') //true
codePointAt()
JavaScript 内部 字符格式 UTF-16(两个字节)。需要4个字节存储的字符(码点大于 0xFFFF 的字符),JavaScript 认为是两个字符。
var s = "𠮷";
let {length: len} = s;
console.log(len)
console.log(s.charAt(0));
console.log(s.charAt(1));
console.log(s.charCodeAt(0));
console.log(s.charCodeAt(1));
//run:
2
�
�
55362
57271
var s = "𠮷a";
console.log(s.codePointAt(0));
console.log(s.codePointAt(1));
console.log(s.codePointAt(2));
console.log(s.codePointAt(0).toString(16));
console.log(s.codePointAt(2).toString(16))
//run:
134071
57271
97
20bb7
61
var s = "𠮷a";
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
//run:
20bb7
61
//判断一个字符是否占4个字节
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("𠮷")); //true
console.log(is32Bit("a")); //false
String.fromCodePoint()
//码点值到字符转换
console.log(String.fromCodePoint(0x20BB7)) //𠮷
console.log(String.fromCodePoint(0x78, 0x1f680, 0x79))
console.log('x\uD830\uDE80y')
console.log(String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y')
//run:
x🚀y
xy
true
字符串遍历接口
for (let ch of 'foo') {
console.log(ch)
}
var text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) { //普通遍历无法遍历码点大于 0xFFFF的字符
console.log(text[i])
}
//run:
�
�
at()
console.log('abc'.charAt(0)); //a
console.log('𠮷'.charAt(0)); //� ECMAScript 7 将支持码点大于 0xFFFF 的字符,chrome已经支持
normalize()
console.log("\u01D1");
console.log("\u004F\u030c");
console.log("\u01D1" === "\u004F\u030c")
console.log("\u01D1".length)
console.log("\u004F\u030c".length)
//run:
Ǒ //单独字符
Ǒ //合成字符
false
1
2
console.log("\u01D1".normalize() === "\u004F\u030c".normalize()) //true
//normalize() Unicode 正规化,将字符的不同表示方法统一为同样的形式。
normalize()
参数:
- NFC,默认参数,标准等价合成,(Normalization Form Canonical Composition),返回多个简单字符的合成字符。标准等价 指 视觉和语义上的等价。
- NFD,Decomposition,标准等价分解,将 NFC 的合成分解成多个简单字符。
- NFKC,兼容等价合成,Normalization Form Compatibility Composition,返回合成字符,语义等价,视觉不等价。
- NFKD,兼容等价分解,NFKC 逆过来。
console.log('\u004f\u030c'.normalize('NFC').length)
console.log('\u004f\u030c'.normalize('NFD').length)
//run:
1
2
includes(), startsWith(), endsWith()
var s = "helloWorld.js";
s.startsWith("hello"); //true
s.endsWith(".js"); //true
s.includes("oWor"); //true
repeat()
'x'.repeat(5);
//xxxxx
'hello'.repeat(2);
//hellohello
let a = 'zero';
console.log(`start${a.repeat(0)}end`); //startend
let a = 'zero';
console.log(a.repeat(2.9)); //zerozero 小数向下取整
let a = 'Zero';
console.log(a.repeat('2')); //ZeroZero
padStart(), padEnd()
for (let i = 1; i < 14; i++) {
console.log(String(i).padStart(3, '0'));
}
//run:
001
002
003
004
005
006
007
008
009
010
011
012
013
for (let i = 1; i < 14; i++) {
console.log(String(i).padStart(5, 'ab'));
}
//run:
abab1
abab2
abab3
abab4
abab5
abab6
abab7
abab8
abab9
aba10
aba11
aba12
aba13
for (let i = 1; i < 14; i++) {
console.log(String(i).padEnd(5, 'ab'));
}
//run:
1abab
2abab
3abab
4abab
5abab
6abab
7abab
8abab
9abab
10aba
11aba
12aba
13aba
模板字符串
//使用 jQuery 进行 DOM 操作,将字符串内容追加到一个 HTML 元素中。
//传统写法
$("#result").append(
"There are <b>" + basket.count + "</b>" +
"items in your basket, " +
"<em>" + backet.onSale +
"</em> are on sale!"
);
//ES6写法
$("#result").append(
`There are <b>${basket.count}</b>
items in your basket
<em>${backet.onSale}</em> are on sale!`
);
// 反引号 `` 包裹的字符串镶嵌变量,镶嵌格式 ${变量名},取变量里面的值替换 ${变量名} 放在原字符串里面。
//反引号里的字符换可以换行,保留原有格式
console.log(`abcd
1234
yes
no`) //字符换原模原样输出,包括空格、换行等
//run:
abcd
1234
yes
no
var x = 1;
var y = 2;
console.log(`x + y = ${x + y}`);
var obj = {
x: 1,
y:2
};
console.log(`the sum of x and y in obj is ${obj.x + obj.y}`);
function fn() {
return "hello world";
}
console.log(`start ${fn()} end`); //start hello world end
引用模板字符串本身:
写法一:
let str = 'return ' + '`Hello ${name}!`'; // 用作函数体
let func = new Function('name', str); //构造函数
console.log(func('Jack'))
new Function('name', str)
创建了一个新的函数。new Function()
是一个构造函数,用于动态创建 JavaScript 函数。这个构造函数允许代码在运行时生成新的函数,函数体的代码以字符串形式提供。
写法二:
let str = '(name) => `Hello ${name}!`'; //字符串的内容是一个箭头函数
//箭头函数接收一个name参数,返回 `Hello ...`
let func = eval.call(null, str);
console.log(func('Jack'));
在 JavaScript 中,箭头函数是一种简洁的函数表示方式,可以使用 =>
定义。
eval
是一个全局函数,它会将字符串作为 JavaScript 代码执行。在这里,eval
被用来将 str
变量中的字符串解析为实际的 JavaScript 函数。
eval.call(null, str)
的作用是将 str
作为参数传递给 eval
函数执行,call(null, ...)
是为了确保 eval
的上下文 (this
) 被设置为 null
。在这种情况下,this
的值是无关紧要的,但使用 call
可以确保 eval
的上下文不会受到干扰。
eval
执行后,str
字符串中的箭头函数会被解析为一个真正的 JavaScript 函数,并赋值给变量 func
。
实例:模板编译
模板编译是一个将模板字符串(通常包含动态数据插值和逻辑代码)转换成可执行的 JavaScript 代码的过程。这个过程的目的是生成最终的 HTML 代码或其他格式的输出,以便于动态内容的渲染。
var template = `
<ul>
<% for(var i=0; i < data.supplies.length; i++) {%>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`; //这个代码片段定义了一个模板字符串,它包含 HTML 结构和嵌入的 JavaScript 逻辑。模板引擎会将这个模板字符串编译成一个函数,函数可以接受数据并生成动态的 HTML 内容。
<% ... %>
: 这是一个包含 JavaScript 逻辑的代码块。这个块内的代码会被执行,但不会直接输出到结果中。
- 在这个例子中,使用了一个
for
循环来遍历data.supplies
数组的元素。
<%= ... %>
: 这是一个输出表达式,用于将 JavaScript 表达式的结果插入到模板中。在这个例子中,它将 data.supplies[i]
的值插入到 <li>
元素中。
编译和渲染:
要将这个模板字符串转换为实际的 HTML,需要使用模板引擎将其编译成函数,并将数据传递给这个函数进行渲染。许多 JavaScript 模板引擎(如 Underscore.js, EJS, Handlebars, 或 Mustache)都提供了这种功能。
const {JSDOM} = require('jsdom'); //npm install jsdom
const dom = new JSDOM(
`<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="myDiv">
</div>
</body>
</html>`)
const { window } = dom;
var template = `
<ul>
<% for(var i=0; i < data.supplies.length; i++) {%>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`; //模板字符串
// echo('<ul>');
// for (var i=0; i < DataTransfer.supplies.length; i++) {
// echo('<li>');
// echo(data.supplies[i]);
// echo('</li>');
// };
// echo('</ul>');
function compile(template) {
var evalExpr = /<%=(.+?)%>/g;
var expr = /<%([\s\S]+?)%>/g;
template = template.replace(evalExpr, '`); \n echo( $1 ); \n echo(`').replace(expr, '`); \n $1 \n echo(`');
template = 'echo(`' + template + '`);';
var script =
`(function parse(data){
var output = "";
function echo(html){
output += html;
}
${ template}
return output;
})`;
return script;
}
const div = window.document.getElementById('myDiv');
var parse = eval(compile(template));
div.innerHTML = parse({supplies : ["broom", "mop", "cleaner"]});
console.log(window.document.documentElement.outerHTML);
<!DOCTYPE html>
<html>
<head>
<script>
alert("Hello,World!");
</script>
</head>
<body>
<div id="myDiv"></div>
</body>
<script>
var template = `
<ul>
<% for(var i=0; i < data.supplies.length; i++) {%>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
function compile(template) {
var evalExpr = /<%=(.+?)%>/g;
var expr = /<%([\s\S]+?)%>/g;
template = template.replace(evalExpr, '`); \n echo( $1 ); \n echo(`').replace(expr, '`); \n $1 \n echo(`');
template = 'echo(`' + template + '`);';
var script =
`(function parse(data){
var output = "";
function echo(html){
output += html;
}
${ template}
return output;
})`;
return script;
}
var parse = eval(compile(template));
var div = this.document.getElementById('myDiv');
div.innerHTML = parse({supplies: ["broom", "mop", "cleaner"]});
</script>
</html>
正则表达式
Regular Expressions (regex) 用于匹配、查找和替换字符串中字符模式的工具。
创建正则表达式:
使用斜杠 (/
) 包围正则表达式的模式:
let regex = /pattern/flags;
pattern
是正则表达式的模式。
flags
是可选的标志,用于控制正则表达式的行为。
构造函数:
可以通过 RegExp
构造函数动态创建正则表达式:
let regex = new RegExp('pattern', 'flags');
正则表达式标志
g
: 全局匹配,不仅匹配第一个,还匹配所有符合条件的字符串。
i
: 忽略大小写匹配。
m
: 多行匹配,使 ^
和 $
匹配行的开头和结尾。
s
: 点号(.
)匹配包括换行符在内的所有字符。
u
: Unicode 模式,支持 Unicode 字符集。
y
: 粘附模式,确保正则表达式从指定的位置开始匹配。
正则表达式模式
基本字符类
\d
: 匹配数字,等价于[0-9]
。\D
: 匹配非数字字符。\w
: 匹配字母、数字和下划线,等价于[a-zA-Z0-9_]
。\W
: 匹配非字母、数字和下划线字符。\s
: 匹配任何空白字符(空格、制表符、换行符等)。\S
: 匹配非空白字符。
字符集和范围
[abc]
: 匹配字符a
、b
或c
。[^abc]
: 匹配除了字符a
、b
或c
之外的字符。[a-z]
: 匹配小写字母中的任何一个。
量词
\*
: 匹配前面的字符零次或多次。+
: 匹配前面的字符一次或多次。?
: 匹配前面的字符零次或一次。{n}
: 匹配前面的字符恰好n
次。{n,}
: 匹配前面的字符至少n
次。{n,m}
: 匹配前面的字符至少n
次,但不超过m
次。
边界匹配
^
: 匹配输入字符串的开始。$
: 匹配输入字符串的结束。
捕获组和非捕获组
()
: 捕获组,用于提取匹配的子字符串。(?:)
: 非捕获组,用于分组但不捕获匹配的内容。
正则表达式方法
RegExp.prototype.test
测试一个字符串是否匹配正则表达式:
let regex = /hello/;
console.log(regex.test('hello world')); // 输出: true
RegExp.prototype.exec
执行正则表达式匹配,返回一个数组或 null
:
let regex = /(\d+)/;
let result = regex.exec('The number is 42');
console.log(result); // 输出: [ '42', '42', index: 16, input: 'The number is 42', groups: undefined ]
String.prototype.match
使用正则表达式匹配字符串,返回匹配结果的数组:
let str = 'The numbers are 42 and 37';
let regex = /\d+/g;
let result = str.match(regex);
console.log(result); // 输出: [ '42', '37' ]
String.prototype.replace
使用正则表达式替换字符串中的匹配项:
let str = 'Hello world';
let result = str.replace(/world/, 'there');
console.log(result); // 输出: Hello there
String.prototype.split
使用正则表达式拆分字符串:
let str = 'one, two, three';
let result = str.split(/,\s*/);
console.log(result); // 输出: [ 'one', 'two', 'three' ]
String.prototype.search
返回值:
- 返回第一个匹配的索引位置(从 0 开始),如果没有匹配项,则返回
-1
。
只能使用正则表达式的特性:
search
方法只支持正则表达式,并且不能使用正则表达式的g
(全局) 标志。如果提供的正则表达式有g
标志,search
方法会忽略这个标志。
let str = 'Hello world!';
let index = str.search(/world/);
console.log(index); // 输出: 6 (world 在字符串中的起始位置是第 6 个字符)
let str = 'Hello World!';
let index = str.search(/world/i); // 使用 i 标志来忽略大小写
console.log(index); // 输出: 6 (world 在字符串中的起始位置是第 6 个字符)
let str = 'Hello!';
let index = str.search(/world/);
console.log(index); // 输出: -1 (没有找到匹配项)
例子
验证邮箱地址
let emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(emailRegex.test('example@example.com')); // 输出: true
1. ^
字符串开始
- 含义: 匹配输入字符串的开始部分。确保整个字符串都符合正则表达式的模式。
2. [a-zA-Z0-9._%+-]+
本地部分
-
- 匹配一个字符集,其中包括:
[a-zA-Z0-9._%+-]
a-z
:所有小写字母A-Z
:所有大写字母0-9
:所有数字._%+-
:特殊字符点(.
)、下划线(_
)、百分号(%
)、加号(+
)和减号(-
)
-
+
: 匹配前面的字符集一次或多次。意味着本地部分可以包含一个或多个允许的字符。
3. @
电子邮件的分隔符
- 含义: 匹配电子邮件地址中的
@
符号,用于分隔本地部分和域名部分。
4. [a-zA-Z0-9.-]+
域名部分
-
- 匹配一个字符集,其中包括:
[a-zA-Z0-9.-]
a-z
:所有小写字母A-Z
:所有大写字母0-9
:所有数字.
:点号(用于分隔域名部分)-
:减号(用于分隔域名的不同部分)
-
+
: 匹配前面的字符集一次或多次。意味着域名部分可以包含一个或多个允许的字符。
5. \.
点号
- 含义: 匹配一个实际的点号
.
。由于点号在正则表达式中有特殊含义(匹配任何字符),所以在这里需要用反斜杠\
转义,表示字面量点号。
6. [a-zA-Z]{2,}
顶级域名
-
- 匹配一个字符集,其中包括:
[a-zA-Z]
a-z
:所有小写字母A-Z
:所有大写字母
-
{2,}
: 匹配前面的字符集至少 2 次。表示顶级域名(如.com
,.org
,.net
等)必须包含至少两个字母字符。
7. $
字符串结束
- 含义: 匹配输入字符串的结束部分。确保整个字符串都符合正则表达式的模式。
提取日期
let dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
let dateStr = '2024-08-20';
let match = dateRegex.exec(dateStr);
if (match) {
console.log(`Year: ${match[1]}, Month: ${match[2]}, Day: ${match[3]}`);
}
1. (\d{4})
年份部分
\d
: 匹配一个数字字符,相当于[0-9]
。{4}
: 表示匹配前面的字符(数字)恰好 4 次。即,匹配 4 位数字。()
: 圆括号用于捕获组,将匹配的内容存储到一个数组中,以便后续引用。在这个例子中,它捕获了年份部分。
2. -
分隔符
- 含义: 匹配一个字面量的连字符
-
。用于分隔日期的不同部分(年、月、日)。
3. (\d{2})
月份部分
\d
: 匹配一个数字字符,相当于[0-9]
。{2}
: 表示匹配前面的字符(数字)恰好 2 次。即,匹配 2 位数字。()
: 圆括号用于捕获组,将匹配的内容存储到一个数组中。在这个例子中,它捕获了月份部分。
4. -
分隔符
- 含义: 匹配一个字面量的连字符
-
,用于分隔日期的不同部分(年、月、日)。
5. (\d{2})
日期部分
\d
: 匹配一个数字字符,相当于[0-9]
。{2}
: 表示匹配前面的字符(数字)恰好 2 次。即,匹配 2 位数字。()
: 圆括号用于捕获组,将匹配的内容存储到一个数组中。在这个例子中,它捕获了日期部分。
RegExp 构造函数
var regex = new RegExp("xyz", "i");
//等价于
var regex = /xyz/i;
//RegExp()接收正则表达式
var regex = new RegExp(/xyz/i);
var regex = /xyz/i;
exStr = "66623asdfggexyz";
console.log(regex.exec(exStr));
//run:
[ 'xyz', index: 12, input: '66623asdfggexyz', groups: undefined ]
new RegExp(/abc/ig, 'i').flags //忽略原有标志ig,改为 i
u标志
// Unicode 模式,处理大于 \uFFFF 的Unicode 字符
console.log('\uD83D\uDC2A') //🐪
console.log(/^\uD83D/u.test('\uD83D\uDC2A')); //false, '\uD83D\uDC2A'是一个4字节的字符
console.log(/\uD83D\uDC2A/u.test('asdfaasdf\uD83D\uDC2Afasdfasd')); //true
console.log(/^\uD83D/.test('\uD83D\uDC2A')); //true, 不加u标志,'\uD83D\uDC2A'会当成两个字符
点字符:
.
匹配除换行以外的任意单个字符。码点大于 0xFFFF 的 Unicode 字符,点字符不能识别,必须加上 u标志。
var s = '𠮷';
console.log(/^.$/.test(s)) //false
console.log(/^.$/u.test(s)) //true
量词:
//使用 u 标志,所有两次都会正确识别码点大于 0xFFFF 的Unicode 字符
var ss = '𠮷𠮷';
var aa = 'aa';
console.log(/a{2}/.test(aa));
console.log(/a{2}/u.test(aa));
console.log(/𠮷{2}/.test(ss));
console.log(/𠮷{2}/u.test(ss));
//run:
true
true
false
true
console.log(/^\u{3}$/.test('uuu')) //true, u被匹配3次
console.log(/^\u{3}$/u.test('uuu')) //false, \u{3} 被当作一个 Unicode字符
预定义模式:
// \S匹配所有不是空格的字符
console.log(/^\S$/.test('𠮷')) //false
console.log(/^\S$/u.test('𠮷')) //true
正确返回字符串长度的函数:(字符串包含Unicode 字符)
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
var s = '𠮷𠮷\uD83D\uDC2A';
console.log(s.length) // 6, 一个 𠮷 被当作两个字符
console.log(codePointLength(s)) //3,正确返回了字符串的长度(字符码点大于 0xFFFF)
console.log('\u004B'); // K
console.log('\u212A'); // K 非规范的字符 K
console.log(/[a-z]/i.test('\u212A')); //false 无法识别非规范字符 K
console.log(/[a-z]/iu.test('\u212A')); // true
y标志
y标志 - 粘连(sticky)。
var s = "aaa_aa_a"
var r1 = /a+/g; //a匹配一次或多次,全局匹配
var r2 = /a+/y; //a匹配一次或多次,粘连匹配
console.log(r1.exec(s)); //['aaa']
console.log(r2.exec(s)); //['aaa']
console.log(r1.exec(s)); //['aa']
console.log(r2.exec(s)); // null
var r = /a+_/y;
console.log(r.exec(s)); //['aaa_']
console.log(r.exec(s)); //['aa_']
//run:
[ 'aaa', index: 0, input: 'aaa_aa_a', groups: undefined ]
[ 'aaa', index: 0, input: 'aaa_aa_a', groups: undefined ]
[ 'aa', index: 4, input: 'aaa_aa_a', groups: undefined ]
null
[ 'aaa_', index: 0, input: 'aaa_aa_a', groups: undefined ]
[ 'aa_', index: 4, input: 'aaa_aa_a', groups: undefined ]
g标志:后一次匹配从上一次匹配成功的下一个位置开始,剩余的字符串中只要有符合正则表达式的就匹配。
y标志:后一次匹配从上一次匹配成功的下一个位置开始,但是要连在一起,中间不能有其他字符(上面的案例中中间有个 _
字符)。
y标志的应用,和 g标志相比,使用y标志可以发现被匹配字符串之间非法的字符。例如:从字符串提取 token 词元,y标志确保匹配之间不会有漏掉的字符。
sticky 属性:
// 正则表达式是否设置 sticky
var regex = /hello\d/y;
console.log(regex.sticky); //true
flags 属性:
// 返回正则表达式的标志
console.log(/abc/ig.flags); //ig
console.log(/abc/ig.source); //abc
数值
二进制八进制:
console.log(0b101 === 5); // 二进制数值 0b 开头
console.log(0o12 === 10); // 八进制数值 0o 开头
console.log(Number(0b101), Number(0o12));
//run:
true
true
5 10
Number.isFinite(), Number.isNaN():
// infinite adj. 无穷的
// finite adj. 有限的
Number.isFinite(15); // 15 是否有限
Number.isFinite(NaN); // false Not a Number 不是一个数字 NaN
Number.isFinite(Infinity); //false
Number.isFinite('15'); //false
Number.isNaN(NaN); //true
Number.isNaN(666); //false
Number.isNaN("999"); //false
Number.isNaN(true); //false
Number.parseInt(), Number.parseFloat()
Number.parseInt('12.34'); //12
Number.parseFloat('3.1415926abc'); // 3.1415926
Number.isInteger()
Number.isInteger(25); //true //是否整数
Number.isInteger(25.0); //true
Number.isInteger(25.3); //false
Number.isInteger('11'); //false
Number.isInteger(true); //false
函数
通过解构赋值设置默认值:
function func({x, y = 1}) {
console.log(x, y);
}
func({}); //undefined 1
func({x:1}); //1 1
func({x:1, y:6}); //1 6
func(); //Error
直接指定默认值:
function func(x, y = "world") {
console.log(x, y);
}
func("hello"); //hello world
func("hello", "Jackey Song"); //hello Jackey Song
func("hello", ""); //hello
function fetch(url, { body='', method='GET', headers={} }) {
console.log(method);
}
fetch('https://jackey-song.com', {}); //GET 解构赋值参数必须指定
fetch('https://jackey-song.com'); //不指定报错
function fetch(url, { body='', method='GET', headers={} } = {}) {
console.log(method);
}
fetch('https://jackey-song.com'); //GET 解构赋值指定默认值,不报错
function f1({x = 0, y = 0} = {}) {
console.log(x, y);
}
function f2({x, y} = {x:0, y:0}) {
console.log(x, y);
}
f1(); //0 0
f2(); //0 0
f1({x: 5, y:4}); //5 4
f2({x: 5, y:4}); //5 4
f1({x: 5}); //5 0
f2({x: 5}) //5 undefined
f1({}); //0 0
f2({}); //undefined undefined
指定默认值的参数应当放在最后一个:
function func(x, y, z=1) {
console.log(x, y, z);
}
func(3,2); //3 2 1
function m(x=1, y, z) {
console.log(x, y, z);
}
m(2,3); //2 3 undefined
m(undefined, 2, 3); //1 2 3 默认值参数放在第一个(或中间),每次使用默认值都要传个 undefined
function n(x, y=1, z) {
console.log(x, y, z);
}
n(2, undefined, 3); //2 1 3
函数的 length 参数个数:
function func(x, y, z=1) {
console.log(x, y, z);
}
console.log(func.length) //2 不会算上默认值
变量作用域:
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2); //2 z在这里函数的参数x是一个局部变量,传入了2,y = x,所以y是2,而不是外部的全局变量1
console.log(...[1, 2, 3]); //1 2 3
函数的name属性:
function func() {
return 0;
}
var abc = function() { //匿名函数赋值给变量 abc
return 0;
}
console.log(func.name); //func 返回函数名
console.log(abc.name); //abc
const f = function func() {
return 0;
}
console.log(f.name); //func
var abc = function f() {
return 0;
}
console.log(abc.name); //f
箭头函数
使用 => 定义函数。
var f = v => v;
//等同于
var f = function(v) {
return v;
}
var f = () => 5; //不需要参数,使用圆括号
//等同于
var f = function() {
return 5
}
var f = (a, b, c) => a + b + c; //多个参数
console.log(f(1,2,3)); //6
//等同于
var f = function(a, b, c) {
return a + b + c;
}
var f = (a, b, c) => {a = a + b + c; return a**2};
//函数体有多个语句,要用花括号,返回值要加 return
console.log(f(1, 1, 1)); //9
var f = name => ({id: 1, name: name});
//返回值是一个对象,对象本身有花括号包裹,因此外面需要再加一个圆括号。
console.log(f("Jackey Song")); //{ id: 1, name: 'Jackey Song' }
[1,2,3]map(function(x) {
return x*x;
});
//简写
[1,2,3]map(x => x*x);
const numbers = (...nums) => nums;
console.log(numbers(1,2,3,4,5)); //[ 1, 2, 3, 4, 5 ]
const headAndTail = (head, ...tail) => [head, tail];
console.log(headAndTail(1,2,3,4,5)); //[ 1, [ 2, 3, 4, 5 ] ]
箭头函数没有自己的 this
绑定。它们从定义时的上下文继承 this
,即箭头函数的 this
是在函数定义时确定的,不是调用时确定的。
function Counter() {
this.value = 0;
setInterval(() => {
this.value++; // `this` 指向 Counter 实例
console.log(this.value);
}, 1000); //每1000毫秒执行一次
}
new Counter();
箭头函数不能用作构造函数,不能使用 new
关键字实例化对象。
箭头函数没有 arguments
对象。如果需要访问函数的参数,可以使用 rest
参数(...args
)。
function traditionalFunction() {
console.log(arguments); // [1, 2, 3]
}
const arrowFunction = () => {
// console.log(arguments); // 会抛出错误
};
traditionalFunction(1, 2, 3);
arrowFunction(1, 2, 3); // `arguments` 在箭头函数中是不可用的
由于箭头函数不会创建自己的 this
绑定,它们在事件处理程序和其他需要 this
绑定的场景中特别有用。
class MyClass {
constructor() {
this.name = 'MyClass';
}
greet() {
setTimeout(() => {
console.log(`Hello from ${this.name}`); // `this` 正确地指向 MyClass 实例
}, 1000);
}
}
new MyClass().greet();
箭头函数不可使用 yield 命令,箭头函数不能用作 Generator 函数。
箭头函数不能使用 super
关键字,因为它们没有自己的 this
,因此无法访问父类的构造函数或方法。
class Parent {
constructor() {
this.name = 'Parent';
}
method() {
return () => {
console.log(super.method()); // 错误: `super` 不能在箭头函数中使用
};
}
}
class Child extends Parent {
constructor() {
super();
this.name = 'Child';
}
method() {
return 'Hello from Child';
}
}
const child = new Child();
child.method()();
function foo() {
setTimeout( () => {
console.log("id:", this.id);
}, 100); //100毫秒后执行
}
//两种调用方式
foo()
foo.call( {id: 42}); //id: 42
1. foo()
调用方式
接调用函数 foo()
时,它会在当前的上下文中执行,而不是使用任何特定的 this
值。
function foo() {
setTimeout(() => {
console.log("id:", this.id);
}, 100); // 100 毫秒后执行
}
foo(); // 在全局上下文中调用
this
绑定:- 在浏览器中,直接调用
foo()
时,this
通常会绑定到全局对象window
(在严格模式下,this
是undefined
)。 - 由于在全局上下文中
id
不存在,这段代码在浏览器的非严格模式下会输出id: undefined
,在严格模式下会抛出错误,因为this
是undefined
,无法读取id
属性。
- 在浏览器中,直接调用
2. foo.call({id: 42})
调用方式
当使用 foo.call({id: 42})
调用函数时,foo
会被执行,并且 this
会被显式地设置为 call
方法中的第一个参数。
function foo() {
setTimeout(() => {
console.log("id:", this.id);
}, 100); // 100 毫秒后执行
}
foo.call({id: 42}); // 在指定的上下文中调用
this
绑定:- 在这种情况下,
this
在foo
函数体内会被绑定到{id: 42}
对象。这意味着箭头函数内部的this
也是{id: 42}
。 - 因此,
setTimeout
回调中的this.id
会正确地输出42
。
- 在这种情况下,
关键区别总结
foo()
: 直接调用时,this
是调用foo
的上下文(全局对象或undefined
),特别是在异步操作中,如setTimeout
,this
的绑定可能会与期望的上下文不同。foo.call(context)
: 使用call
方法调用时,this
被显式地设置为context
参数。在异步操作中,this
的值将根据call
的参数来确定。
示例对比
示例 1: foo()
function foo() {
setTimeout(() => {
console.log("id:", this.id);
}, 100);
}
foo(); // 输出: "id: undefined" 或者在严格模式下可能报错
示例 2: foo.call({id: 42})
function foo() {
setTimeout(() => {
console.log("id:", this.id);
}, 100);
}
foo.call({id: 42}); // 输出: "id: 42"
其他代码:
var handler = {
id: "123456",
init: function() {
document.addEventListener("click", event => this.doSomething(event.type), false); //箭头函数内部的 this 指向 handler 对象
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id); //这里的 this 指向全局对象
}
};
function Timer () {
this.seconds = 0; //this 指向new Timer() 创建的对象
setInterval(() => this.seconds++, 1000); //this 指向该对象
}
var timer = new Timer();
setTimeout(() => console.log(timer.seconds), 3100); //3
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
foo.call({id: 42})()()(); //id: 42
//箭头函数本身没有自己的 this,所以无论箭头函数嵌套多少层,它都是外层的 this。正因为它没有自己的this,所以它不能用作构造函数。
构造函数 VS 普通函数
1. 函数名称的约定
-
构造函数:
- 构造函数通常以大写字母开头,以便与普通函数区分。这是一个约定而非强制规则。例如:
Person
,Car
,Timer
等。
function Person(name) { this.name = name; }
- 构造函数通常以大写字母开头,以便与普通函数区分。这是一个约定而非强制规则。例如:
-
普通函数:
- 普通函数的名称通常以小写字母开头,虽然这并不是强制的,但可以帮助区分。例如:
calculateTotal
,printMessage
等。
function calculateTotal(a, b) { return a + b; }
- 普通函数的名称通常以小写字母开头,虽然这并不是强制的,但可以帮助区分。例如:
2. new
关键字
-
构造函数:
- 构造函数应使用
new
关键字调用。当用new
调用构造函数时,它会创建一个新对象并将this
绑定到这个新对象。
const person = new Person('Alice'); // 使用 new 调用构造函数
- 构造函数应使用
-
普通函数:
- 普通函数不应该用
new
关键字调用。如果用new
调用普通函数,它将不会按预期工作,通常会返回undefined
或导致错误。
const total = calculateTotal(5, 10); // 普通函数的调用方式
- 普通函数不应该用
3. this
绑定
-
构造函数:
- 在构造函数内部,
this
指向正在被创建的新对象。构造函数可以将属性和方法添加到这个新对象上。
function Person(name) { this.name = name; // `this` 指向新创建的 Person 对象 }
- 在构造函数内部,
-
普通函数:
- 在普通函数中,
this
的值取决于函数的调用方式。在非严格模式下,this
通常指向全局对象(在浏览器中是window
),在严格模式下,this
是undefined
。
function printName() { console.log(this.name); // `this` 取决于调用上下文 }
- 在普通函数中,
4. 返回值
-
构造函数:
- 构造函数通常不显式返回值。它们自动返回新创建的对象。如果显式地返回一个对象,那个对象将作为构造函数的结果被返回。
function Person(name) { this.name = name; // 不需要返回值,自动返回新对象 }
-
普通函数:
- 普通函数可以返回任何值,包括原始数据类型、对象或其他函数。返回值必须显式地指定。
function add(a, b) { return a + b; // 显式返回结果 }
5. 使用场景
-
构造函数:
- 主要用于创建和初始化对象实例。它们用于定义对象的属性和方法,通常通过
new
关键字创建新实例。
function Car(make, model) { this.make = make; this.model = model; } const myCar = new Car('Toyota', 'Corolla');
- 主要用于创建和初始化对象实例。它们用于定义对象的属性和方法,通常通过
-
普通函数:
- 主要用于执行特定的任务或计算结果。普通函数可以被调用多次,但不创建对象实例。
function multiply(a, b) { return a * b; } const result = multiply(4, 5);
总结
-
构造函数:
- 名称以大写字母开头(通常为约定)。
- 使用
new
关键字调用。 - 在函数内部,
this
绑定到新创建的对象。 - 通常不显式返回值(自动返回新对象)。
-
普通函数:
- 名称以小写字母开头(虽然这不是强制的)。
- 不使用
new
关键字调用。 this
取决于调用上下文。- 可以显式返回值。
函数绑定
函数绑定是 JavaScript 中处理 this
的一种机制,允许创建一个新的函数并预设其中的 this
值和/或参数。函数绑定主要有三种实现方式:Function.prototype.bind()
方法、.call()
和 .apply()
方法的使用,以及箭头函数的 this
绑定行为。
1. Function.prototype.bind()
方法
bind()
方法用于创建一个新的函数,这个新函数会将 this
绑定到指定的对象,并且可以预先设置一些参数。
-
语法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
-
参数:
thisArg
: 新函数执行时this
的值。arg1
,arg2
, …: 预设的参数,调用新函数时这些参数会被默认传入。
-
示例:
function greet(greeting, name) { console.log(`${greeting}, ${name}!`); } const greetHello = greet.bind(null, 'Hello'); greetHello('Alice'); // 输出: "Hello, Alice!"
在这个例子中,
greetHello
是一个新的函数,它的this
被绑定为null
,并且greeting
参数被预设为'Hello'
。调用greetHello('Alice')
实际上等价于greet(null, 'Hello', 'Alice')
。
2. .call()
和 .apply()
方法
call()
和 apply()
方法用于立即调用函数,并可以显式地设置函数的 this
值和参数。
-
call()
方法:function functionName(arg1, arg2, ...) { // function body } functionName.call(thisArg, arg1, arg2, ...);
-
apply()
方法:function functionName(arg1, arg2, ...) { // function body } functionName.apply(thisArg, [arg1, arg2, ...]);
-
区别:
call()
接受参数列表。apply()
接受一个参数数组。
-
示例:
function greet(greeting, name) { console.log(`${greeting}, ${name}!`); } greet.call(null, 'Hi', 'Bob'); // 输出: "Hi, Bob!" greet.apply(null, ['Hi', 'Bob']); // 输出: "Hi, Bob!"
在这两个例子中,
greet
函数的this
被设置为null
,并且传入的参数被分别通过call()
和apply()
方法传递。
3. 箭头函数的 this
绑定
箭头函数的 this
绑定规则不同于普通函数。箭头函数不创建自己的 this
上下文,它会从外围上下文中继承 this
。因此,箭头函数中的 this
是静态绑定的,在函数创建时就已经确定。
总结
bind()
: 创建一个新函数,设置this
和预设参数。不会立即执行。call()
和apply()
: 立即调用函数,设置this
和传递参数。call()
接受参数列表,apply()
接受参数数组。
函数绑定是 JavaScript 中处理 this
绑定和函数调用的关键技术,在各种上下文中控制 this
的值和行为。
对象
创建对象
-
对象字面量(Object Literal):
这是创建对象最直接和常用的方法。const person = { name: 'John', age: 30, greet: function(name) { console.log(`Hello! ${name}`); } }; person.greet("Jackey Song");
-
构造函数(Constructor Function):
定义一个构造函数,然后用new
关键字创建对象。function Person(name, age) { this.name = name; this.age = age; this.greet = function() { console.log('Hello!'); }; } const person = new Person('John', 30); person.greet();
-
Object.create()
方法:
使用Object.create()
方法可以创建一个新的对象,指定其原型。const personPrototype = { greet: function() { console.log('Hello!'); } }; const person = Object.create(personPrototype); person.name = 'John'; person.age = 30; person.greet = function () { //重写原型中的 greet() 方法 console.log(`hello, my name is${this.name}, and I am ${this.age} years old.`); } person.greet();
-
class
语法:
使用 ES6 的class
语法来定义一个类,然后创建对象实例。class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name}, My age is ${this.age}`); } } const person = new Person('John', 30); person.greet();
-
Object.assign()
方法:
Object.assign()
可以用于复制对象的属性。虽然它不是直接用来创建对象的方法,但可以用来从现有对象创建新对象。const person = Object.assign({}, { name: 'John', age: 30, greet: function() { console.log(`I am ${this.name}. My age is ${this.age}.`); } }); person.greet();
-
工厂函数(Factory Function):
工厂函数是一个普通函数,用于创建并返回一个新的对象。function createPerson(name, age) { return { name: name, age: age, greet: function() { console.log('Hello!'); } }; } const person = createPerson('John', 30); person.greet();
这些方法中,对象字面量
和 class
语法是最直观和常用的,而 Object.create()
和 Object.assign()
提供了更多的灵活性。
var foo = 'bar';
var baz = {foo}; //对象的属性直接设置为变量名
console.log(baz); //{ foo: 'bar' }
function f(x, y) {
return {x, y}; //将变量直接封装到对象中返回
}
console.log(f("abc", "123")); //{ x: 'abc', y: '123' }
var o = {
method() { //对象封装方法
return "hello world!";
}
}
属性赋值器 setter 、取值 getter:
//工厂函数
function createStorage() {
const storage = {};
return {
getItem: function(key) {
return key in storage ? storage[key] : null;
},
setItem: function(key, value) {
storage[key] = value;
},
clear: function() {
for (let key in storage) {
if (storage.hasOwnProperty(key)) {
delete storage[key];
}
}
}
};
}
const storage = createStorage();
storage.setItem("name", "Jackey Song");
storage.setItem("age", "18");
console.log(storage.getItem("name")); // 输出: Jackey Song
console.log(storage.getItem("age")); // 输出: 18
storage.clear();
console.log(storage.getItem("name")); // 输出: null
console.log(storage.getItem("age")); // 输出: null
//直接写入对象中
var obj = {
setItem: function(key, value) {
this[key] = value;
},
getItem: function(key) {
return key in this ? this[key] : null;
},
clear: function() {
for (let key in this) {
if (this.hasOwnProperty(key)) {
delete this[key];
}
}
}
};
obj.setItem("name", "Jackey Song");
obj.setItem("age", "18");
console.log(obj.getItem("name")); // 输出: Jackey Song
console.log(obj.getItem("age")); // 输出: 18
obj.clear();
console.log(obj); // 输出: {}
//封装成类
class Storage {
constructor() {
this.storage = {};
}
getItem(key) {
return key in this.storage ? this.storage[key] : null;
}
setItem(key, value) {
this.storage[key] = value;
}
clear() {
this.storage = {};
}
}
const storage = new Storage();
storage.setItem("name", "Jackey Song");
storage.setItem("age", "18");
console.log(storage.getItem("name")); // 输出: Jackey Song
console.log(storage.getItem("age")); // 输出: 18
storage.clear();
console.log(storage.getItem("name")); // 输出: null
console.log(storage.getItem("age")); // 输出: null
类
在 JavaScript 中,类(class
)是 ES6 引入的用于创建对象和处理继承的一种语法糖。它提供了一种更简洁的方式来定义构造函数和继承结构。
1. 定义类
使用 class
关键字来定义一个类。一个类包含构造函数和方法。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person('Jackey', 1000);
person.greet(); // 输出: Hello, my name is Jackey and I am 1000 years old.
2. 构造函数
constructor
是一个特殊的方法,用于初始化类的新实例。每次创建类的实例时,constructor
方法会被调用。
3. static
//静态方法
class MathUtils {
static add(a, b) { //static 修饰的方法是静态方法
return a + b;
}
static multiply(a, b) { //静态方法只能通过类名调用
return a * b;
}
constructor(name){ //构造方法会在创建对象的时候调用
this.name = name;
}
sub(a, b) { //普通方法
return a - b;
}
}
console.log(MathUtils.add(2, 3)); // 输出: 5
console.log(MathUtils.multiply(2, 3)); // 输出: 6
var mth = new MathUtils();
console.log(mth.sub(1,2)); //可以调用普通方法
console.log(mth.add(1,2)); //静态方法不能通过对象调用
//静态属性
//静态属性也是通过 static 关键字定义。静态属性属于类本身,而不是类的实例。它们可以用于存储与类相关的数据,但不是特定于任何实例的。
class Car {
static wheels = 4;
static getWheels() {
return Car.wheels;
}
}
console.log(Car.wheels); // 输出: 4
console.log(Car.getWheels()); // 输出: 4
//静态方法可以被子类继承和调用。子类可以直接访问父类的静态方法,但子类不会继承父类实例方法。
class Vehicle {
static start() {
console.log('Vehicle started');
}
}
class Car extends Vehicle {}
Car.start(); // 输出: Vehicle started
4. 继承
JavaScript 的类可以继承其他类。使用 extends
关键字来创建子类,并使用 super
关键字来调用父类的构造函数和方法。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
super.speak(); //在子类的方法内部使用 super 来调用父类的方法。
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Rex');
dog.speak();
// 输出: Rex makes a noise.
// Rex barks
5. Getter 和 Setter
类中的 getter 和 setter 用于定义对象属性的访问和设置逻辑。
class Person {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length > 0) {
this._name = value;
}
}
}
const person = new Person('Jackey');
console.log(person.name); // 输出: Jackey
person.name = 'John';
console.log(person.name); // 输出: John
6. 私有属性
ES2022 引入了私有字段,用于定义类的私有属性。这些属性只能在类的内部访问。
class Person {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const person = new Person('Jackey');
console.log(person.getName()); // 输出: Jackey
// console.log(person.#name); // 语法错误
异步
在 JavaScript 中,异步编程允许执行长时间运行的操作(如网络请求、文件读取等)而不会阻塞程序的其他部分。异步编程有助于提高应用的响应性和性能。以下是 JavaScript 中异步编程的主要概念和技术:
1. 回调函数(Callbacks)
回调函数是最基础的异步编程技术。将一个函数作为参数传递给另一个函数,并在某个操作完成后调用这个回调函数。
示例:
function fetchData(callback) {
setTimeout(() => {
const data = 'Some data';
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data); // 输出: Some data
});
console.log("hello"); //先打印 hello ,一秒后箭头函数被回调,才打印 Some data
2. Promise
Promise
是一种更现代的异步处理机制,代表一个操作的最终完成(或失败)及其结果值。它允许你链式地处理多个异步操作。
创建 Promise:
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = 'Some data';
resolve(data); // 成功时调用 resolve
// reject('Error occurred'); // 失败时调用 reject
}, 1000);
});
};
fetchData()
.then(data => {
console.log(data); // 输出: Some data
})
.catch(error => {
console.error(error);
});
console.log("hello")
Promise 状态:
Pending
(待定):初始状态,既不是成功也不是失败。Fulfilled
(已兑现):操作成功完成。Rejected
(已拒绝):操作失败。
3. Promise.all 和 Promise.race
Promise.all
:接受一个包含多个 Promise 对象的数组,只有所有 Promise 都成功时,才会成功,返回一个包含每个 Promise 结果的数组。
const promise1 = Promise.resolve('Data from promise1');
const promise2 = Promise.resolve('Data from promise2');
Promise.all([promise1, promise2])
.then(results => {
console.log(results); // 输出: ['Data from promise1', 'Data from promise2']
})
.catch(error => {
console.error(error);
});
Promise.race
:接受一个包含多个 Promise 对象的数组,返回第一个完成的 Promise 的结果(不论成功还是失败)。
const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 500, 'Result from promise1'));
const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 100, 'Result from promise2'));
Promise.race([promise1, promise2])
.then(result => {
console.log(result); // 输出: Result from promise2
});
4. async/await
async
和 await
是 ES2017(ES8)引入的异步编程语法糖,使异步代码看起来更像同步代码。async
用于定义异步函数,await
用于等待 Promise 解析。
示例:
const fetchData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Some data');
}, 1000);
});
};
const processData = async () => {
try {
const data = await fetchData(); // 等待 fetchData 完成
console.log(data); // 输出: Some data
} catch (error) {
console.error(error);
}
};
processData();
特点:
async
:声明函数为异步函数,返回一个 Promise。await
:在异步函数内部,暂停代码执行直到 Promise 解决,简化了异步操作的代码。
5. 错误处理
在异步编程中,错误处理是非常重要的。对于 Promise
,可以使用 .catch
处理错误;对于 async/await
,可以使用 try/catch
。
Promise 错误处理:
fetchData()
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
async/await 错误处理:
const processData = async () => {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
};
6. 其他异步 API
JavaScript 还提供了其他异步 API,例如:
setTimeout
和setInterval
:用于延迟执行代码或定期执行代码。fetch
API:用于网络请求,基于 Promises。XMLHttpRequest
:传统的网络请求 API,较老,使用回调函数处理异步操作。
- 回调函数:最基础的异步处理方式。
- Promise:提供链式调用和更好的错误处理。
Promise.all
和Promise.race
:用于处理多个 Promise。async/await
:使异步代码看起来像同步代码,简化异步编程。- 错误处理:无论使用何种异步技术,正确的错误处理是关键。
模块
JavaScript 的模块系统允许将代码组织成独立的、可重用的部分,这样可以更好地管理和维护大型应用程序。
ES6 模块提供了 export
和 import
关键字,用于在不同的 JavaScript 文件之间共享代码。
打开或创建 package.json
文件,添加 "type": "module"
属性。
{
"type": "module"
}
1. 导出(export
)
export
关键字用于将模块中的变量、函数或类暴露给其他模块。可以有两种导出方式:
- 具名导出(Named Exports):导出一个或多个具体的变量、函数或类。
// math.js
export const pi = 3.14;
export function add(a, b) {
return a + b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
}
- 默认导出(Default Export):每个模块只能有一个默认导出。默认导出用于导出一个主值(如函数、类或对象)。
// utils.js
export default function greet(name) {
return `Hello, ${name}!`;
}
结合具名导出和默认导出:
// utils.js
export const version = '1.0';
export default function greet(name) {
return `Hello, ${name}!`;
}
2. 导入(import
)
import
关键字用于从其他模块中导入导出的内容。导入时,需遵循相应的导出方式:
- 导入具名导出:
// app.js
import { pi, add, Calculator } from './math.js';
console.log(pi); // 输出: 3.14
console.log(add(2, 3)); // 输出: 5
const calc = new Calculator();
console.log(calc.multiply(2, 3)); // 输出: 6
- 导入默认导出:
// app.js
import greet from './utils.js';
console.log(greet('World')); // 输出: Hello, World!
- 导入所有内容:使用
* as
语法可以导入一个模块的所有导出,并将它们作为一个对象来使用。
// app.js
import * as math from './math.js';
console.log(math.pi); // 输出: 3.14
console.log(math.add(2, 3)); // 输出: 5
3. 重新导出
可以在一个模块中重新导出其他模块的导出内容:
// index.js
export { pi, add } from './math.js';
export { default as greet } from './utils.js';
4. 动态导入
ES6 模块还支持动态导入,这允许在运行时按需加载模块。动态导入返回一个 Promise 对象,因此可以使用 .then()
或 await
处理。
// app.js
async function loadModule() {
const { default: greet } = await import('./utils.js');
console.log(greet('Dynamic Import'));
}
loadModule();
5. 模块的作用域
每个模块在 JavaScript 中都有自己的作用域。模块中的变量和函数默认是私有的,只有通过 export
才能公开给其他模块。
export
:用于导出模块中的内容,可以是具名导出或默认导出。import
:用于从其他模块导入内容,可以导入具名导出、默认导出,或将所有导出作为一个对象导入。- 动态导入:允许在运行时按需加载模块。
- 模块作用域:每个模块有自己的作用域,避免了全局命名冲突。