知识点梳理
目录
变量类型
ECMAScript 中定义了 6 种原始类型:( ECMAScript 是一种规格, javascript 是 这种规格的实现 )
6种: Boolean String Number Null Undefinded Symbol
注意:原始类型不包含 Object。
typeof
typeof xxx得到的值有以下几种类型:undefined boolean number string object function、symbol
instanceof
用于实例和构造函数的对应。例如判断一个变量是否是数组,使用typeof无法判断,
但可以使用[1, 2] instanceof Array来判断。
因为,[1, 2]是数组,它的构造函数就是Array。
值类型 vs 引用类型
值类型变量包括 Boolean、String、Number、Undefined、Null,
引用类型包括了 Object 类的所有,如 Date、Array、Function 等
// 引用类型
var a = {x: 10, y: 20}
var b = a
b.x = 100
b.y = 200
console.log(a) // {x: 100, y: 200}
console.log(b) // {x: 100, y: 200}
提高题目
①
function foo(a){
a = a * 10;
}
function bar(b){
b.value = 'new';
}
var a = 1;
var b = {value: 'old'};
foo(a);
bar(b);
console.log(a); // 1 这里是因为 Number 类型的 a 是按值传递, 而 Object 类型的b是按照共享传递是
console.log(b); // value: new
②
var obj = {
a: 1,
b: [1,2,3]
}
var a = obj.a
var b = obj.b
a = 2
b.push(4)
console.log(obj, a, b)
输出
{ a: 1, b: [ 1, 2, 3, 4 ] }
2
[ 1, 2, 3, 4 ]
虽然obj本身是个引用类型的变量(对象),
但是内部的a和b一个是值类型一个是引用类型,
a的赋值不会改变obj.a,但是b的操作却会反映到obj对象上。
Symbol 类型
特点
- 唯一性: 通过 Symbol() 创建的符号都是唯一的
- 不可变性: 一旦创建就无法被更改
- 私有性: Symbol 可以作为 对象私有属性键, 这些属性不会被 for…in 或者 Object.keys() 等迭代方法访问
创建唯一的属性键
// 创建一个 Symbol
const myUniqueKey = Symbol('myUniqueKey');
// 使用 Symbol 作为对象的属性键
const myObject = {
[myUniqueKey]: 'This value is unique'
};
// 访问这个属性
console.log(myObject[myUniqueKey]); // 输出: This value is unique
// 尝试使用常规字符串作为键访问这个属性
console.log(myObject['myUniqueKey']); // 输出: undefined
避免属性名冲突
当你在不同的地方扩展同一个对象时,使用 Symbol 可以避免属性名冲突。
const mySymbol = Symbol('myProperty');
const obj1 = {
[mySymbol]: 'Value from obj1'
};
const obj2 = {
[mySymbol]: 'Value from obj2'
};
console.log(obj1[mySymbol]); // 输出: Value from obj1
console.log(obj2[mySymbol]); // 输出: Value from obj2
// 这两个属性是唯一的,即使它们在不同的上下文中使用了相同的 Symbol
私有属性
在JavaScript中,没有真正的私有属性,但 Symbol 可以用来模拟私有属性。
const privateKey = Symbol('privateKey');
class MyClass {
constructor() {
this[privateKey] = 'I am a private property';
}
getPrivateProperty() {
return this[privateKey];
}
}
const myInstance = new MyClass();
console.log(myInstance.getPrivateProperty()); // 输出: I am a private property
// 尝试直接访问这个属性会失败,因为它看起来像是一个普通属性
console.log(myInstance.privateKey); // 输出: undefined
使用 Symbol.for 和 Symbol.keyFor
Symbol.for 允许你创建或访问一个全局的 Symbol,而 Symbol.keyFor 可以获取一个 Symbol 对应的字符串键。
// 创建一个全局 Symbol
const globalSymbol = Symbol.for('globalSymbol');
// 访问这个全局 Symbol
const retrievedGlobalSymbol = Symbol.for('globalSymbol');
console.log(globalSymbol === retrievedGlobalSymbol); // 输出: true
// 获取 Symbol 的字符串描述
console.log(Symbol.keyFor(globalSymbol)); // 输出: 'globalSymbol'
Symbol.for() 和 Symbol() ** 创建的有什么区别?
当你定义一个全局的 Symbol** 值时,通常是通过调用 Symbol.for(key) 方法,
其中 key 是一个字符串,用来描述这个 Symbol。
如果已经存在一个具有相同描述的 Symbol,
则 Symbol.for(key) 会返回那个已经存在的 Symbol,
否则会创建一个新的 Symbol 并将其注册到全局符号注册表中。
直接使用 Symbol() 创建的 Symbol 是一个全新的、唯一的 Symbol,
没有注册到全局符号注册表中,
它只存在于当前执行的代码环境中。
// ECMAScript 是一种规格 有六种类型
// 其中 Symbol 就是一种类型
// Symbol.for() 与 Symbol() 有什么区别
const Symbol1 = Symbol('xiaohei') // 注册到当前代码环镜
const Symbol2 = Symbol.for('xiaohei') // 创建到全局注册表
console.log(Symbol1, Symbol2)
console.log(Symbol.keyFor(Symbol1)) // unidentified
console.log(Symbol.keyFor(Symbol2)) // xiaohei
let obj = {
[Symbol1]: '我是obj'
}
console.log(obj[Symbol1])
console.log(obj[Symbol2])
// 通过 Symbol() 直接创建是唯一的
const symbol3 = Symbol('foo');
const symbol4 = Symbol('foo');
console.log(Symbol1 === Symbol2); // false
console.log(symbol3 === symbol4); // false
原型与原型链
所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性(null除外)
所有的引用类型(数组、对象、函数),都有一个__proto__属性,属性值是一个普通的对象
所有的函数,都有一个prototype属性,属性值也是一个普通的对象
所有的引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值
// 要点一:自由扩展属性
var obj = {}; obj.a = 100;
var arr = []; arr.a = 100;
function fn () {}
fn.a = 100;
// 要点二:__proto__
console.log(obj.__proto__);
console.log(arr.__proto__);
console.log(fn.__proto__);
// 要点三:函数有 prototype
console.log(fn.prototype)
// 要点四:引用类型的 __proto__ 属性值指向它的构造函数的 prototype 属性值
console.log(obj.__proto__ === Object.prototype)
原型
所有 JS 对象 (包括函数) 都有一个内部属性 [[ Prototype ]], 这个属性指向一个对象或者null。
这个指向的**对象 ** , 我们称为对象的原型。
// 构造函数
function Foo(name, age) {
this.name = name
}
Foo.prototype.alertName = function () {
alert(this.name)
}
// 创建示例
var f = new Foo('zhangsan') // 通过 new 对象实例化 f 是 Foo 的一个实例, 也是一个对象
f.printName = function () {
console.log(this.name)
}
// 测试
f.printName()
f.alertName()
执行printName时很好理解,但是执行alertName时发生了什么?
这里再记住一个重点
当试图得到一个对象的某个属性时,
如果这个对象本身没有这个属性,
那么会去它的__proto__(即它的构造函数的prototype)中寻找,
因此f.alertName就会找到Foo.prototype.alertName。
接着上面的示例
f.printName()
f.alertName()
f.toString()
因为f本身没有toString(),并且 f.proto(即Foo.prototype)中也没有toString。
这个问题还是得拿出刚才那句话——当试图得到一个对象的某个属性时,
如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的 prototype)(在这里就是Car 对象 )中寻找。
小结
<script>
function Person() {
}
let p = new Person();
console.log(p);
if (p.__proto__ === Person.prototype) {
console.log(true);
}
if (Person.prototype.__proto__ === Object.prototype) {
console.log(true);
}
if (Object.prototype.__proto__ === null) {
console.log(true);
}
</script>
大家可以回头自行理解一下这份代码,理解什么是隐式原型,什么是显式原型,而原型链又是如何形成的。
图解
Person 是一个构造函数,
显示原型: 通过 prototype属性 可以得到 Person 的原型
隐式原型: 利用__proto__
属性 可以查找原型, 这个属性是对象类型数据的属性
原型链
在上面我们知道 __proto__
属性是一个对象类型数据的属性,那么上面的 Person.prototype 是一个对象
那么该对象的 __proto__
指向哪里?
我们需要找 __proto__
指向哪里, 就需要找到构建该对象的构建函数,
就比如,我们要找 per1.__proto__
, 就需要找到 构建函数 Person , per1.__proto__
指向的就是 Person.prototype ( 显示原型 )
而js中,对象的构造函数就是Object(),所以对象的原型对象,就是Object.prototype。
不过Object.prototype这个比较特殊,它没有上一层的原型对象,或者说是它的__proto__指向的是null。
函数也是一种对象
函数在js中,也算是一种特殊的对象,所以,可以想到的是,函数是不是也有一个__proto__属性?
答案是肯定的,既然如此,那就按上面的思路,先来找找函数对象的构造函数。
在js中,所有函数都可以看做是Function()的实例,
而Person()和Object()都是函数,
所以它们的构造函数就是Function()。
Function()本身也是函数,所以Function()也是自己的实例,
听起来既怪异又合理,但是就是这么回事。
console.log(Person.constructor === Function); // true
console.log(Object.constructor === Function); // true
console.log(Function.constructor === Function); // true
总的来说, 需要记住两点
- 所有对象的构建函数是 Object()
- 所有函数的构建函数是 Function()
- 构造函数是使用了new关键字的函数,用来创建对象,所有函数都是Function()的实例
- 原型对象是用来存放实例对象的公有属性和公有方法的一个公共对象,所有原型对象都是Object()的实例
code
// 函数类型数据
function foo() {
this.a = 1
console.log("foo");
}
// 添加公共方法
foo.prototype.sayHello = function (name) {
console.log("Hello world!", name);
};
const foo1 = new foo()
console.log("foo1隐式原型对象", foo1.__proto__);
console.log("foo函数显示原型", foo.prototype)
console.log("foo函数的隐式原型",foo.__proto__)
console.log("Function函数的原型", Function.prototype)
console.log(foo.__proto__ === Function.prototype
实现继承
// 定义一个构造函数 Dog,继承自 Animal
function Dog(name, breed) {
Animal.call(this, name); // 调用 Animal 的构造函数
this.breed = breed;
}
// 让 Dog 的原型指向新的 Animal 实例
// 非常关键
// 这行代码的作用是创建了一个新的空对象,其原型指向 Animal.prototype。
// 然后,这个新对象被赋值给 Dog.prototype。
// 这样,Dog 的实例就会继承 Animal.prototype 上定义的所有属性和方法
Dog.prototype = Object.create(Animal.prototype);
// 修正构造函数的引用
Dog.prototype.constructor = Dog;
// 在 Dog 的原型上添加一个方法
Dog.prototype.bark = function () {
console.log(`${this.name} barks.`);
};
// 创建 Animal 和 Dog 的实例
const animal = new Animal("Generic Animal");
const dog = new Dog("Rex", "German Shepherd");
// 演示原型链
animal.speak(); // Generic Animal makes a noise.
dog.speak(); // Rex makes a noise.
dog.bark(); // Rex barks.
// 验证一下
console.log(Dog.prototype === Animal.prototype) // false
console.log(Dog.prototype.__proto__ === Animal.prototype) // true
Dog.prototype = Object.create(Animal.prototype);的意思是
创建一个新的空对象, 其内部的[[Prototype]] 指向 Animal.prototype,
所以 console.log(Dog.prototype.proto === Animal.prototype) // true
所以 Dog.prototype 的原型是 Animal.prototype,
所以 Dog 的实例可以访问在 Animal.prototype 上定义的方法,例如 speak 方法。
注意后面需要重设 constructor 属性
Object.create()
我们传入一个对象{name: ‘johan’, age: 23}, 发现 obj 对象的隐式原型为 {name: ‘johan’, age: 23}
所以上面
let obj = Object.create({name: 'johan', age: 23})
console.log(obj) // {}
console.log(obj.__proto__) // {name: 'johan', age: 23}
console.log(obj.name) // johan
方法2
把 new Animal() 对象赋值给 Dog 原型
// 动物
function Animal() {
this.eat = function () {
// console.log(this)
console.log(`${this.name} eat`)
}
}
// 狗
function Dog() {
this.bark = function () {
console.log('dog bark')
}
}
Dog.prototype = new Animal()
// 哈士奇
function Husky(name) {
this.name = name;
Dog.call(this);
}
Husky.prototype = Dog.prototype;
var hashiqi = new Husky('hashiqi');
hashiqi.bark();
hashiqi.eat();
console.log(Dog.prototype.constructor)
// dog bark
// hashiqi eat
// [Function: Animal]
浏览器渲染过程 ( 非常重要需要用自己话讲出来 )
解析HTML
解析HTML的过程可以分为以下几个步骤:
- 标记化(Tokenization):浏览器将HTML文档分割成一个个标记,包括开始标签、结束标签、属性和文本内容等。这些标记被称为令牌(Tokens)。
- 构建节点对象:对于每个令牌,浏览器会创建相应的节点对象,并将其添加到DOM树中。节点对象可以分为元素节点、文本节点、注释节点等不同类型。
- 构建父子关系:浏览器会根据开始标签和结束标签之间的嵌套关系,构建父子关系。即将子节点添加到父节点下。
- 处理属性:对于开始标签中的属性,浏览器会解析并将其转换为节点对象的属性。每个属性都包含一个名称和一个值,浏览器会将它们存储在节点对象中,以便后续使用。
- 处理文本内容:对于文本内容,浏览器会创建文本节点,并将其添加到相应的父节点下。这些文本节点表示HTML文档中的纯文本内容。
- 处理注释和其他特殊标记:除了元素和文本节点外,HTML文档还可以包含注释、DOCTYPE声明、CDATA节和其他特殊标记。浏览器也会解析并处理这些特殊标记。
- 错误处理:在解析HTML过程中,如果遇到不符合规范的标记或语法错误,浏览器会尽可能地进行容错处理。它会尝试修复错误,并继续构建DOM树。但是,有些错误可能无法修复,导致DOM树无法完全构建。
在解析 HTML 的过程中,我们可以能会遇到诸如 style、link 这些标签,这是和我们网页样式相关的内容。此时就会涉及到 CSS 的解析。
如果渲染主线程解析到 link 位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,而是继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。
最终,CSS 的解析在经历了从字节数据、字符串、标记化后,最终也会形成一颗 CSSOM 树
预解析线程除了下载外部 CSS 文件以外,还会下载外部 JS 文件,如果主线程解析到 script 位置,会停止解析HTML,等待 JS 文件下载好,并将全局代码解析并执行完成后,才能继续解析 HTML。
- 解析JavaScript代码:当浏览器遇到JavaScript代码时,会逐行解析并执行代码。解析过程包括词法分析、语法分析和生成抽象语法树(AST)等步骤。
- 执行JavaScript代码:一旦解析完成,浏览器会执行JavaScript代码。代码中可能包含对DOM树的操作、事件处理、网络请求等操作。
- 修改DOM和样式:JavaScript代码可以通过操作DOM树和CSS样式来修改页面的结构和样式。这可能会触发重新构建渲染树、布局计算和绘制页面的过程。
- 异步加载:浏览器还支持异步加载JavaScript文件,可以通过**
因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以需要等待js执行完成。这就是 JS 会阻塞 HTML 解析的根本原因。 这也是都建议将 script 标签放在 body 标签底部的原因。
<html>
<head>
...
</head>
<body>
<div></div>
<script src="..."></script>
</body>
</html>
第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
DOM 树
CSS对象模型( CSSOM )
样式计算、得到渲染树
拥有了 DOM 树我们还不足以知道页面的外貌,因为我们通常会为页面的元素设置一些样式。主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。
- 选择器匹配:对于每个节点,浏览器会遍历CSSOM树,匹配适用于该节点的所有选择器。这个过程称为选择器匹配(Selector Matching)。浏览器会从右向左进行匹配,并使用各种优化策略来提高匹配效率。
- 计算最终样式:一旦确定了适用于节点的所有选择器,浏览器会计算这些选择器对应的最终样式。它会考虑选择器的优先级、继承规则和层叠顺序等因素。
- 属性值计算:对于每个最终样式属性,浏览器会计算其具体值。这可能涉及到单位转换、颜色计算、字体渲染等操作。
- 继承处理:某些样式属性具有继承性,即子节点会继承父节点的样式。浏览器会根据继承规则,将适用的父节点样式属性应用到子节点上。
- 计算结果存储:最后,浏览器会将计算得到的最终样式存储在**渲染树(Render Tree)**中的每个节点上。渲染树只包含需要显示的节点和其对应的样式信息。
布局
前面这些步骤完成之后,渲染进程就已经知道页面的具体文档结构以及每个节点拥有的样式信息了,可是这些信息还是不能最终确定页面的样子。还需要通过布局(layout)来计算出每个节点的几何信息(geometry)。
生成布局树的具体过程是:
- 布局计算:对于每个节点,浏览器会进行布局计算,确定其在屏幕上的位置和大小。这个过程是递归进行的,从渲染树的根节点开始,逐级向下计算。
- 盒模型计算:对于每个元素节点,浏览器会计算其盒模型(Box Model)。盒模型包括元素的内容区域、内边距、边框和外边距等部分。浏览器会考虑CSS样式中设置的宽度、高度、内外边距等属性,并结合父元素和兄弟元素的布局信息进行计算。
- 文字排版:对于包含文本内容的节点,浏览器会进行文字排版。这涉及到字体渲染、行高计算、文本折行等操作,以确定文本在元素内的布局。
- 流式布局:布局计算过程中,浏览器会根据元素的定位属性(如position、float等)和文档流(Flow)规则,确定元素在页面中的位置。这包括块级元素的垂直排列和行内元素的水平排列等操作。
- 尺寸调整:在布局计算过程中,如果发现某个节点的尺寸发生变化(例如内容变化、窗口大小调整等),浏览器会重新进行布局计算,以确保页面的正确显示。
- 布局结果存储:最后,浏览器会将布局计算得到的位置和大小信息存储在渲染树中的每个节点上。这些信息将用于后续的绘制过程。
需要注意的是,布局是一个相对耗时的操作,特别是在处理大型复杂页面时。为了提高性能,浏览器会使用一些优化策略,例如增量更新、异步布局等。同时,开发者也可以通过优化CSS样式、减少DOM操作和避免频繁修改尺寸等方式来提升页面渲染性能。
绘制
在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点;
包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img)
总结
- 解析HTML:浏览器会将接收到的HTML代码进行解析,构建DOM树。
- 样式计算:浏览器会解析CSS样式表,构建CSSOM树。然后将DOM树和CSSOM树结合起来,生成渲染树。
- 布局:布局计算会确定每个元素在屏幕上的位置和大小。浏览器会遍历渲染树,计算每个元素的布局信息。
- 分层:为了提高渲染性能,浏览器会将渲染树分成多个图层。每个图层都有自己的绘制指令和绘制顺序。
- 生成绘制指令:浏览器会遍历图层,并生成绘制指令。绘制指令描述了如何绘制每个元素,包括颜色、边框等信息。
- 分块:为了提高渲染性能,浏览器会将页面划分成多个块(或称为矩形区域)。每个块都有自己的绘制指令。
- 光栅化:光栅化是将矢量图形转换为位图的过程。浏览器会将每个块的绘制指令转换为位图。
- 绘制:浏览器会将位图绘制到屏幕上。这个过程包括将位图合成、混合和显示。
整个流程是一个逐步迭代的过程,从解析HTML开始,经过样式计算、布局、分层、生成绘制指令、分块、光栅化和绘制等多个步骤,最终完成页面的显示
JS 的作用域
补充
以下是 var
和 let
的主要区别:
- 声明提升:
var
声明的变量会进行声明提升,可在声明前,值为undefined
;let
声明的变量不会提升,必须在声明后使用,否则会报错。
console.log(a); // 不会报错,输出 undefined
var a = 1;
console.log(a); // 报错
let a = 1;
- 块级作用域:
var
没有块级作用域,let
具有块级作用域。
var 声明的变量具有函数作用域或全局作用域。如果在任何函数外部声明,它将成为全局变量。
即使在块级作用域(如if语句或for循环内)中声明,var 变量也会提升到包含它的函数或全局作用域的顶部。
let 提供了块级作用域,这意味着变量只在它被声明的代码块(例如,if语句、for循环或一对花括号内)中可用。
if(true){
var a=1
}
console.log(a); // 输出 1
if(true){
let a=1
}
console.log(a); // a is not defined
- 重复声明:
var
允许在同一作用域内多次声明同名变量,let
不允许。 - 暂时性死区:
let
声明的变量在变量声明之前处于暂时性死区,任何访问都会导致错误。
let a=1
if(1){
console.log(a); // 暂时性死区
let a=2
} // 编译报错 Cannot access 'a' before initialization
JS 执行上下文
例子
var g = 10;
function fn(h) {
h = 20;
console.log("fn",g, h);
}
function fn1() {
g = 100;
fn(g);
}
fn1();
每个执行上下文主要由三部分组成:
-
变量对象(Variable Object, VO):存储变量和函数声明。
-
作用域链(Scope Chain):确定变量和函数的可访问性。
-
this 值:确定当前执行代码的执行环境。
详细执行过程分析 -
全局执行上下文(Global Execution Context, GEC)
创建变量对象(Global VO):初始化 g 为 undefined。
创建作用域链:全局作用域链只有一个元素,即全局对象(在浏览器中是 window)。
this 值:指向全局对象。
将 GEC 压入执行上下文栈。 -
变量和函数声明提升
变量提升:var g; 被提升到 GEC 的变量对象中。
函数提升:fn 和 fn1 的函数声明被提升,但它们的函数体不会提升。 -
执行全局代码
执行到 var g = 10;,此时 g 在全局变量对象中的值被初始化为 10。
执行到函数声明 function fn(h) {…} 和 function fn1() {…},这些函数现在可以被调用。 -
调用 fn1 函数
创建 fn1 的执行上下文(Local Execution Context, LEC):
创建变量对象,初始化参数和局部变量(这里没有局部变量)。
创建作用域链,fn1 的作用域链包括它自己的变量对象和全局变量对象。
确定 this 值,通常在非严格模式下指向全局对象。 -
fn1 执行上下文压栈
fn1 的 LEC 被压入执行上下文栈顶部。 -
在 fn1 中修改 g ( 开始执行 fn1 代码 )
在 fn1 中执行 g = 100;,修改全局变量对象中的 g 值为 10 -
调用 fn 函数
创建 fn 的执行上下文:
创建变量对象,初始化参数 h 和可能的局部变量(这里没有额外的局部变量)。
创建作用域链,fn 的作用域链包括它自己的变量对象、fn1 的变量对象和全局变量对象。
确定 this 值。 -
fn 执行上下文压栈
fn 的 LEC 被压入执行上下文栈顶部,覆盖 fn1 的上下文。 -
在 fn 中执行代码
执行 h = 20;,参数 h 的值变为 20,但不影响传递给它的 g 值。
执行 console.log(“fn”, g, h);,打印 g(值为 100)和 h(值为 20)。 -
fn 执行完毕
fn 的执行上下文从执行上下文栈中弹出。 -
返回到 fn1 执行上下文
控制权返回到 fn1 的执行上下文,fn1 执行完毕。 -
fn1 执行完毕
fn1 的执行上下文从执行上下文栈中弹出。 -
返回到全局执行上下文
控制权返回到全局执行上下文,继续执行任何剩余的代码(在这个例子中,没有剩余代码)。 -
程序结束
全局执行上下文中的所有代码执行完毕,程序结束。
特别注意
这里 fn 在执行console(g) 时, 先在自己局部作用查找有没有 g 变量, 发现没有, 就会到上层作用域查找
这里需要注意的一点,上层作用域和调用函数位置没有关系 和 函数定义位置有关系,
这里的 fn 就会到全局作用域查找,而不是 fn2 ,因为 fn 函数 并不是定义在 fn1 函数里面,仅仅是在fn1 里面进行调用
为了验证这一点,我下面举个例子
var g = 10;
function fn(h) {
console.log("前面",h)
h = 20;
console.log("fn",g, h);
}
function fn1() {
var g = 100;
h = 11
fn(g);
}
fn1(); // fn 10 20
console.log(h) // 11
从上面打印的结果可以知道, fn 并没有去 fn1 作用域中的局部变量 100
下面改变一下函数声明位置
var g = 10;
function fn(h) {
console.log("前面", h);
h = 20;
console.log("fn", g, h);
}
function fn1() {
g = 100;
h = 11;
var aa = 110;
fn(g);
return function fn2() {
console.log("fn2", aa);
};
}
// fn(200); //输出结果fn1(); // 输出结果复制代码
let fu2 = fn1();
fu2(); // fn2 110
JS 执行原理, 了解一下 Event Loop 事件循环
闭包
简单来说
如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包
闭包会创建一个包含外部函数作用域变量的环境,并将其保存在内存中
这意味着,即使外部很熟已经执行完毕,闭包仍然可以访问和使用外部函数的变量
闭包的应用场景
函数柯里化
节流防抖
自执行函数
链式调用
迭代器
发布 - 订阅模式
自执行函数
let say = (function(){
let val = 'hello world';
function say(){
console.log(val);
}
return say;
})()
发布 - 订阅 模式
function createPubSub() {
// 存储事件及其对应的订阅者
const subscribers = {};
// 订阅事件
function subscribe(event, callback) {
// 如果事件不存在,则创建一个新的空数组
if (!subscribers[event]) {
subscribers[event] = [];
}
// 将回调函数添加到订阅者数组中
subscribers[event].push(callback);
}
// 发布事件
function publish(event, data) {
// 如果事件不存在,则直接返回
if (!subscribers[event]) {
return;
}
// 遍历订阅者数组,调用每个订阅者的回调函数
subscribers[event].forEach((callback) => {
callback(data);
});
}
// 返回订阅和发布函数
return {
subscribe,
publish,
};
}
// 使用示例
const pubSub = createPubSub();
// 订阅事件
pubSub.subscribe("event1", (data) => {
console.log("订阅者1收到事件1的数据:", data);
});
pubSub.subscribe("event2", (data) => {
console.log("订阅者2收到事件2的数据:", data);
});
// 发布事件
pubSub.publish("event1", "Hello");
// 输出: 订阅者1收到事件1的数据: Hello
pubSub.publish("event2", "World");
// 输出: 订阅者2收到事件2的数据: World
注意
createPubSub函数:这是一个工厂函数,它创建并返回一个包含subscribe和publish方法的对象。
subscribers对象:在createPubSub函数内部定义了一个subscribers对象,用来存储事件名和对应的订阅者
subscribe函数:这是createPubSub返回对象的一个方法。它是一个闭包,因为它可以访问subscribers对象。当调用> subscribe时,它检查subscribers对象中是否已经有了指定事件名的数组,如果没有,就创建一个新的数组,然后将提供的回调函数添加到这个数组中。
publish函数:这也是createPubSub返回对象的一个方法,同样是一个闭包。它能够访问subscribers对象,并能够遍历特定事件名对应的订阅者数组,调用每个订阅者的回调函数,将数据传递给它们。
闭包的使用示例:
当pubSub.subscribe(“event1”, …)被调用时,subscribe函数能够访问并修改subscribers对象,这是闭包的一个应用。
当pubSub.publish(“event1”, …)被调用时,publish函数也能够访问subscribers对象,找到"event1"对应的订阅者数组,并执行每个订阅者的回调函数,这也是闭包的应用。
闭包在这里非常有用,因为它允许subscribe和publish函数在createPubSub函数外部被调用,同时仍然能够访问和修改createPubSub函数内部的状态(即subscribers对象)。这种模式有助于封装状态,使得状态管理更加安全和可控。
第一个链接有解决闭包造成的内存泄漏问题的方法
闭包应用场景
函数作为返回值
function F1() {
var a = 100
return function () {
console.log(a)
}
}
function F2(f1) {
var a = 200
console.log(f1())
}
var f1 = F1()
F2(f1)
函数作为参数传递
function F1() {
var a = 150
return function () {
console.log("hhh",a)
return a
}
}
function F2(f1) {
var a = 200
console.log(f1())
}
var f1 = F1()
F2(f1)
案例理解之 循环注册事件
比如: 可以使用 闭包的特性 做循环点击事件, 比如 下面给输入框添加 onblur 事件
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{ 'id': 'email', 'help': 'Your e-mail address' },
{ 'id': 'name', 'help': 'Your full name' },
{ 'id': 'age', 'help': 'Your age (you must be over 16)' }
];
for (var i = 0; i < helpText.length; i++) {
// var func = function (i) {
// document.getElementById(helpText[i].id).onfocus = function () {
// showHelp(helpText[i].help);
// }
// };
// func(i);
(function (i) {
document.getElementById(helpText[i].id).onfocus = function () {
showHelp(helpText[i].help);
}
})(i);
}
}
setupHelp();
</script>
理解
可以结合前面学到的 JS 代码执行来理解
在执行代码前, JS 引擎会对 代码 进行分析
比如这里, 在执行到 for 循环前
在 setupHelp 的 AO 中有赋值的 helpText, 此时 i 为 undefined
执行 for 循环时, 第一次循环, func 被赋值一个地址(一个函数),
但是 函数没有被执行 function () { showHelp(helpText[i].help); }
后面 func(i) 调用后,开始执行, 实现绑定
下面也是如此
(function (i) {
document.getElementById(helpText[i].id).onfocus = function () {
showHelp(helpText[i].help);
}
})(i);
思考
它们的i
不是同一个。在setupHelp
函数中,变量i
是一个循环变量,用于遍历helpText
数组。然而,每次循环迭代中,使用IIFE(立即执行函数表达式)时,都会创建一个新的词法作用域,并且在这个新作用域中捕获当前迭代的i
值。
这里的关键点是,每次调用IIFE时,都会将当前的i
值作为一个实参传递给IIFE的形参(在这个例子中是index
)。这样,每个IIFE都会捕获并保存它被调用时i
的值,即使随后i
的值在循环中继续增加。
为什么它们不是同一个 i ?
- 词法作用域:每次IIFE调用都会创建一个新的词法作用域。在这个新作用域中,
index
(IIFE的参数)被赋予了当前循环迭代中i
的值。 - 变量捕获:IIFE中的
index
参数捕获了它被调用时的i
值。这意味着即使外部的i
变量继续变化,IIFE内部的index
仍然保持它被调用时的值。 - 闭包:每个IIFE形成了一个闭包,它包含了对
index
的持久访问,即使IIFE本身是在setupHelp
函数之外执行的。 - 事件处理器:每个事件处理器都引用了它自己的
index
值,而不是setupHelp
函数中的i
。这就是为什么每个输入框的onfocus
事件处理器能够显示正确的帮助信息。
代码示例:
for (var i = 0; i < helpText.length; i++) {
// 立即执行函数表达式,为每次循环创建新的词法作用域
(function (index) {
// 这里,index是i的副本,它保存了调用时i的值
document.getElementById(helpText[index].id).onfocus = function () {
// 这个回调函数可以访问index,即使外部的i已经变化
showHelp(helpText[index].help);
};
})(i); // 将当前的i值传递给IIFE
}
在这个例子中,每个IIFE都捕获了它自己的index
值,即使所有IIFE都是由同一个setupHelp
函数调用的。这就是为什么每个输入框都能够正确地显示其对应的帮助信息,而不是所有输入框都显示最后一个输入框的帮助信息。
防抖以及节流
防抖
当某个函数持续,频繁的触发,那么只在它最后一个函数,且一段时间内没有再次触发,这个函数才会执行,拿一个按钮的点击事件来说,当你多次点击这个按钮并且触发事件对应的回调函数,这个函数将不会执行,只有当你最后一次点击之后,且规定的时间之内没有再次去点击这个按钮,这个回调才会触发!
一般防抖的应用场景有:
输入框搜索:当用户在搜索框中输入关键字时,使用防抖可以避免频繁发送搜索请求,而是在用户停止输入一段时间后才发送请求,减轻服务器压力。
按钮点击:当用户点击按钮时,使用防抖可以避免用户多次提交或重复操作
<body>
<button id="btn">提交</button>
<script>
function send(e){
console.log(this,'已提交',e);
}
let btn = document.getElementById("btn");
btn.addEventListener("click", debounce(send,1000))
function debounce(fn, delay){
let timer
return function(){
let args = arguments
if(timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this,...args)
},delay)
}
}
</script>
</body>
code2
function debounce(fn, delay) {
let timer;
console.log(timer)
return function () {
let args = arguments;
// 清除定时器
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this, ...args);
}, delay);
};
}
function doSomething(arg) {
console.log(arg);
}
const debouncedDoSomething = debounce(doSomething, 1000);
debouncedDoSomething("第一次调用"); // 不会立即执行
debouncedDoSomething("第二次调用"); // 重置定时器,之前设
防抖的好处
减少不必要的计算和渲染:对于一些高开销的操作,如网络请求、复杂的计算或者DOM操作,防抖可以确保这些操作只在用户停止操作一段时间后执行一次,从而避免了因连续快速的操作导致的重复计算和界面频繁刷新,提高了性能和用户体验。
提高效率和响应速度:通过减少不必要的函数执行,可以有效减轻CPU和内存的负担,尤其是在移动设备或性能较低的设备上,这一点尤为重要。同时,因为减少了请求或计算的数量,使得真正需要处理的请求能够更快得到响应。
节省网络资源:对于需要发起网络请求的场景,如搜索建议、表单验证等,防抖可以避免因用户快速输入而导致的多次无谓请求,从而节约了网络带宽和服务器资源。
提升用户体验:频繁的操作和响应可能会让用户感到界面“卡顿”或难以控制,防抖确保了只有在用户完成操作(如停止输入、停止滚动等)后才执行相应的动作,使得界面反应更加自然流畅。
节流技术确保函数在指定的时间间隔内最多执行一次。
无论事件触发多少次,函数都会以固定的时间间隔执行。
这适用于控制窗口的滚动、调整大小等频繁触发的事件。
例子
今天咱们就指定一个规则,这个规则就是我们的服务器在2s内只会处理一条请求,怎么实现呢?来看代码:
<button id="btn">提交</button>
<script>
let btn = document.getElementById('btn')
function send(){
console.log('提交了');
}
btn.addEventListener('click',throttle(send,2000))
function throttle(fn,delay){
let prevTime = Date.now()
return function(){
if(Date.now()-prevTime>delay){
fn.apply(this,arguments)
prevTime =Date.now() // 记录处理时间
}
}
}
</script>
应用场景
滚动事件监听,用于懒加载图片或滚动到顶部/底部的提示。
鼠标移动或触摸屏滑动事件的处理,用于平滑动画效果。
实时表单验证,限制验证频率以提升性能。
节流好处
控制函数执行频率,避免过度消耗资源。
保持操作的连续性反馈,比如在滚动事件中保持平滑的滚动体验。
在不影响用户体验的前提下减少计算负担。
提升系统的稳定性
防抖与节流的区别?
防抖(Debounce)和节流(Throttle)是JavaScript中用于控制函数执行频率的两种技术。它们在处理频繁触发的事件(如窗口调整大小、滚动、按键等)时非常有用。尽管它们的目的相似,但它们的工作方式和适用场景有所不同。
防抖(Debounce)
- 定义:防抖技术确保函数在指定的时间间隔结束后才执行一次。如果在指定的时间间隔内再次触发事件,则会重置计时器,重新开始计时。
- 触发时机:防抖函数在最后一次触发事件后,经过指定的延迟时间才执行。
- 适用场景:适用于搜索框输入、按钮点击等场景,其中希望在用户完成输入或操作后再执行函数。
- 执行保证:防抖函数不保证函数一定会执行,如果在指定的间隔时间内事件持续触发,则函数将不会执行。
- 示例效果:如果用户在搜索框中快速输入并停止,防抖函数会等待用户停止输入一段时间后才执行一次,以获取最终的输入值。
节流(Throttle)
- 定义:节流技术确保函数在指定的时间间隔内最多执行一次。无论事件触发多少次,函数都会以固定的时间间隔执行。
- 触发时机:节流函数在每个指定的时间间隔内至少执行一次,如果在时间间隔内多次触发事件,只有第一次会立即执行,之后直到时间间隔结束后才会再次执行。
- 适用场景:适用于滚动事件、窗口调整大小等场景,其中希望在事件频繁发生时限制函数的执行频率。
- 执行保证:节流函数保证在每个时间间隔内至少执行一次函数,即使事件触发的频率很高。
- 示例效果:如果用户持续滚动页面,节流函数会以固定的时间间隔(例如每秒)执行一次,而不是对每次滚动都做出响应。
区别总结:
- 执行时机:防抖是延迟执行直到停止触发事件,节流是固定时间间隔内执行一次。
- 触发频率:防抖可以在事件触发频率很高时完全不执行,节流则保证无论触发多频繁,都会以固定间隔执行。
- 适用情况:防抖适合用在不需要即时反应的场景,节流适合用在需要持续反应但限制频率的场景。
- 实现方式:防抖通常使用
setTimeout
与clearTimeout
实现,节流可以通过setTimeout
或记录上次执行时间的方式实现。