1、JavaScript作用域
JavaScript作用域:全局作用域和函数作用域
全局作用域
- 全局作用域在页面打开时被创建,页面关闭时被销毁
- 编写在script标签中的变量和函数,作用域为全局,
在页面的任意位置都可以访问到 - 在全局作用域中有全局对象
window,代表一个浏览器窗口,由浏览器创建,可以直接调用 - 在全局作用域中声明的变量和函数会作为
window对象的属性和方法保存
函数作用域
- 调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁
- 每调用一次函数就会创建一个
新的函数作用域,他们之间是相互独立的 - 在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量
- 在函数作用域中访问变量、函数时,会先在自身作用域中寻找,若没有找到,
则会到函数的上一级作用域中寻找,一直到全局作用域
2、预编译
- JavaScript有两个特性,一个是
单线程,一个是解释性语言。 - 不同于编译性语言,
解释性语言通常理解为不整体编译,由解释器一句执行一句,但是JavaScript不是直接对着代码解析执行,在解析执行之前,需要对其进行其他的步骤。
JavaScript运行步骤:
- 语法分析
- 预编译
- 解释执行
全局作用域的预编译
- 创建GO对象
GO{},在开始预编译时产生的对象,比AO对象先产生,用于存放全局变量,也称为全局作用域 - 找
变量声明,将变量名作为GO对象的属性名,值为undefined - 找函数声明,将函数名当做
GO对象的属性名,值为函数体若(函数名和参数名重名,则函数体值会覆盖参数体值)
NOTE:
GO--- global object(全局对象,等同于window)
函数作用域的预编译
- 创建AO对象
AO{},在函数执行前执行函数预编译,此时会产生一个AO对象,AO对象保存该函数的参数变量 - 找
形参和变量声明,将形参和变量,当做AO对象的属性名,值为undefined - 实参与形参相互统一(将实参的值赋值给形参)
- 寻找函数中的函数声明,将函数名当做
AO对象的属性名,值为函数体(若函数名和参数名重名,则函数体值会覆盖参数体值)
NOTES:
AO--- activation object(活跃对象/执行期上下文)var bar = function () {}不是函数声明,function foo () {}是函数声明
fn(1, 2); // 在函数执行前,执行函数预编译
function fn(a, c) {
console.log(a);
var a = 123;
console.log(a);
console.log(c);
function a() {}
if (false) {
var d = 678;
}
console.log(d);
console.log(b);
var b = function () {};
console.log(b);
function c() {}
console.log(c);
}
跟着上面的4步一起来分析一下预编译过程吧
第一步:创建AO对象
const AO = {
};
第二步:找形参和变量声明,将形参和变量,当做AO对象的属性名,值为undefined
const AO = {
// 形参
a: undefined,
c: undefined,
// 变量
d: undefined,
b: undefined,
};
第三步:实参和形参相统一,即将实参的值赋值给形参。
a: undefined --> 1c: undefined --> 2
const AO = {
// 形参
a: 1,
c: 2,
// 变量
d: undefined,
b: undefined,
};
第四步:寻找函数中的函数声明,将函数名作为AO对象的属性名,值为函数体
a: 1 --> function a() {}c: 2 --> function c() {}
const AO = {
// 形参
a: function a() {},
c: function c() {},
// 变量
d: undefined,
b: undefined,
};
至此,预编译阶段已经完成,类似如下过程,接下去是解释执行的过程,按照代码顺序一条条执行。
const AO = {
a: undefined --> 1 --> function a() {}
c: undefined --> 2 --> function c() {}
d: undefined
b: undefined
};
fn(1, 2); // 在函数执行前,执行函数预编译
function fn(a, c) {
console.log(a); // function a() {}
var a = 123;
console.log(a); // 123
console.log(c); // function c() {}
function a() {}
if (false) {
var d = 678;
}
console.log(d); // undefined
console.log(b); // undefined
var b = function () {};
console.log(b); // function () {}
function c() {}
console.log(c); // function c() {}
}
再看一例子
test(1); // 在函数执行前,执行函数预编译
function test(a) {
console.log(d); // function d() {}
console.log(a); // function a() {}
var a = 2;
console.log(a); // 2
function a() {}
console.log(a); // 2
console.log(b); // undefined
var b = function () {};
console.log(b); // function () {}
function d() {}
}
预编译过程
const AO = {
// 形参
a: undefined --> 1 --> function a() {}
// 变量
b: undefined
d: function d() {}
};
3、this指向
非箭头函数中this指向
两个原则
- 原则一:函数直接使用,this此时指向windows
- 原则二:函数作为对象的方法被调用,谁调用我,this就指向谁
原则一:函数直接使用,this此时指向windows
function get(content) {
console.log(content);
}
get("你好"); // 你好
// 上面的是下面的语法糖
get.call(window, "你好"); // 你好
var people = "outPeople";
function hello() {
let people = "innnerPeople";
console.log("hello", this.people);
}
hello(); // hello outPeople
// 上面的是下面的语法糖
hello.call(window); // hello outPeople
原则二:函数作为对象的方法被调用,谁调用我,this就指向谁
var person = {
name: "大明",
run: function (time) {
console.log(`${this.name}在跑步 最多${time}min就不行了`);
},
};
var student = {
name: "学生",
};
person.run(30); // 大明在跑步 最多30min就不行了
// 上面的是下面的语法糖
person.run.call(person, 30); // 大明在跑步 最多30min就不行了
// 原则:谁调用我,我就指向谁,person调用run(),则this指向person
// 改变调用函数的主体为student,student调用run(),则this指向student
person.run.call(student, 20); // 学生在跑步 最多20min就不行了
面试题1
var name = 222;
var a = {
name: 111,
say: function () {
console.log(this.name);
},
};
var fun = a.say;
fun(); // 函数直接使用,this指向window,打印222
a.say(); // 函数作为对象的方法被调用,this指向a,打印111
面试题2
var name = 222;
var a = {
name: 111,
say: function () {
console.log(this.name);
},
};
var fun = a.say;
fun(); // 222
a.say(); // 111
var b = {
name: 333,
say: function (fun) {
fun();
},
};
b.say(a.say); // 函数直接使用,this指向window,打印222
b.say = a.say;
b.say(); // / 函数作为对象的方法被调用,this指向b,打印333
箭头函数中this指向
- 箭头函数中的this是在定义函数的时候绑定的,而不是在执行函数的时候绑定的。
- 箭头函数中,this指向的固定化,并不是因为剪头函数内部有绑定this的机制,实际原因是因为剪头函数根本没有自己的this,导致内部的this就是外层代码块的this。
- 剪头函数自身没有this,因此它不能用作构造函数。
面试题1
var x = 11;
var obj = {
x: 22,
say: () => {
console.log(this.x);
},
};
obj.say(); // 11
解释
- 所谓的定义时候绑定,就是
this是继承自父执行上下文中的this - 比如这里的箭头函数中的
this.x,箭头函数本身与say平级以key:value的形式,也就是箭头函数本身所在的对象为obj - 而
obj的父执行上下文就是window,因此这里的this.x实际上表示的就是window.x,因此输出的是11
面试题2
var obj = {
birth: 1990,
getAge: function () {
var fn = () => new Date().getFullYear() - this.birth;
//this指向obj对象 2022-1990=32
return fn(); //32
},
};
obj.getAge();
解释
箭头函数本身是在getAge方法中定义的,因此,getAge方法的父执行上下文是obj 因此这里的this指向的是obj对象。
4、闭包
先来看一个简单例子
函数fnA中定义了一个变量a和一个函数fnB,并且函数fnA的返回值是fnB。
fn = fnA(),当fnA函数被调用时,fnA函数作用域被创建,当fnA函数执行完毕,fnA函数作用域被销毁,最后返回fnB。fn(),当函数fn函数被调用时,fn函数作用域(也就是fnB函数作用域)被创建,可以知道,fnA函数作用域已经被销毁,但是在函数fn调用的时候,可以访问到fnA函数作用域中的变量a,这时就出现了闭包。
function fnA() {
let a = 1;
function fnB() {
console.log(a);
}
return fnB;
}
var fn = fnA();
fn(); // 1
闭包的特点
- 函数嵌套函数
- 函数的返回值是函数
- 内部函数可以访问到其外部函数作用域中的参数和变量
闭包的定义
MDN对JavaScript闭包的理解
- 一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
- 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
- 在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
coderwhy老师对JavaScript闭包的理解
- 一个普通的函数function,如果它可以访问到外层作用域的自由变量,那么这个函数就是一个闭包
- 从广义的角度来说:JavaScript中的函数都是闭包
- 从狭义的角度来说:Javascript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包
闭包的使用
经典面试题1
循环中使用闭包解决var定义函数的问题:在for循环时,给data数组中元素分别设置函数,最后调用时其实都是执行console.log(i),在for循环结束后此时i = 3了,所以调用函数打印的值都为3。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
使用闭包
var data = [];
for (var i = 0; i < 3; i++) {
(function (j) {
data[i] = function () {
console.log(j);
};
})(i);
}
data[0]();
data[1]();
data[2]();
经典面试题2
循环中使用闭包解决var定义函数的问题:因为setTimeout()是个异步函数,在执行下面for循环时,会将整个循环全部执行完毕,之后再去执行setTimeout()函数中的内容,这个时候i = 5,所以会输出一堆5。
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
使用闭包解决
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, j * 1000);
})(i);
}
闭包的优缺点
优点
- 可以访问其他函数内部的变量
- 变量长期驻扎在内存中,不会被垃圾回收机制回收,即延迟了变量的生命周期
- 避免定义全局变量所造成的污染
缺点
- 不正当地使用闭包可能会造成内存泄漏
如何解决闭包造成的内存泄露:将外部的引用关系置空即可
let fooArr = foo()执行之后fooArr其实存储的是一个函数的引用地址,将该引用地址置空即可解决内存泄露问题。
function foo() {
let arr = new Array(1000).fill(1);
return function () {
console.log(arr.length);
};
}
let fooArr = foo();
fooArr();
fooArr = null; // 置空
NOTE:JavaScript中常见的内存泄露
- 意外的全局变量
- 遗忘的定时器
- 使用不当的闭包
- 遗漏的dom元素
5、事件委托及事件流
事件委托
事件委托就是利用事件冒泡,把原来需要绑定在子元素的相应事件委托给父元素,让父元素担当事件监听的职务。
经典 ui > li 的例子
将子元素li的点击事件绑定在父元素ul上,让父元素担当事件监听的职务。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ul id="ul">
<li>0</li>
<li>1</li>
<li>2</li>
...
<li>9999</li>
</ul>
<script>
window.onload = function () {
var uli = document.getElementById("ul");
uli.onclick = function (event) {
console.log(event.target.innerText);
};
};
</script>
</body>
</html>
event中target和currentTarget的区别
当点击2时,打印target和currentTarget
e.target:触发事件的元素e.currentTarget:绑定事件的元素
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ul id="ul">
<li>0</li>
<li>1</li>
<li>2</li>
...
<li>9999</li>
</ul>
<script>
window.onload = function () {
var uli = document.getElementById("ul");
uli.onclick = function (event) {
console.log("target",event.target);
console.log("currentTarget",event.currentTarget);
};
};
</script>
</body>
</html>
事件流
HTML中和JS交互是通过事件驱动来实现的,例如鼠标点击事件onclick,页面滚动事件onscroll,可以向文档或者文档中的元素添加事件侦听器来监听事件。
事件流:描述的是从页面中接受事件的顺序。
JavaScript事件流的三个阶段
- 捕获阶段:window对象 ---> 目标节点
- 目标阶段:在目标节点上触发
- 冒泡阶段:目标节点 ---> window对象
6、JavaScript数据类型
基本数据类型
- string
- Number
- Boolean
- Null
- undefined
- Symbol
- BigInt
引用数据类型:Object、Function、Array、Date、RegExp等
7、JS中Null和undefined的区别
相同点:
- 用于判断时,两者都会被转换成false
- 都是基本类型的值,保存在栈中
不同点:
typeof的值不同Number()转换的值不同Null表示一个值被定义,但是这个值是空值;undefined表示声明了变量但是未赋值
// typeof的值不同
console.log(typeof null); // object
console.log(typeof undefined); // undefined
// Number()转换的值不同
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
8、变量声明:var let const
var声明的变量会挂载到window对象上,而let和const不会var变量存在变量提升,let和const也存在变量提升但有暂存死区- 同一作用域下,
var可以声明同名变量,let和const不可以 let和const声明会形成块级作用域
9、如何判断一个数据是不是NaN
NaN:Not a number
特点:
- typeof 类型为 number:
typeof NaN ---> "number" - 我不等于我自己:
NaN == NaN ---> false Object.is(NaN, NaN) ---> trueisNaN(NaN) ---> true
const judgeNaN = function (data) {
// 同时满足两个特点,即可判断为NaN
if (typeof data == "number" && data !== data) return true;
return false;
};
10、typeof typeof typeof null 是什么?
console.log(typeof null); // object
console.log(typeof typeof null); // string
console.log(typeof typeof typeof null); // string
typeof操作符返回一个字符串,表示操作值的类型。因此,后面两个都为string。
11、cookie、localStorage、sessionStorage
生命周期
- cookie:可设置失效时间,没有设置的话,默认是浏览器关闭后失效
- localStorage:除非手动删除,否则一直存在
- sessionStorage:仅在当前网页会话下有效,关闭页面或者关闭浏览器会被清除
存放大小
- cookie:4KB左右
- localStorage:5MB
- sessionStorage:5MB
12、GET和POST的异同
相同点
GET和POST方法是HTTP协议为了不同分工而规定的两种请求方式,而HTTP协议是基于TCP/IP的关于数据如何在万维网中通信的协议,HTTP的底层是TCP/IP,所以GET和POST的底层也是TCP/IP,所以它们的本质是相同的。
不同点
- 分工不同
-
- GET用于请求类似于查找的过程
-
- POST一般是修改和删除的工作
- 参数传递方式不同
-
- GET的参数一般是在URL后面通过?拼接,多个参数通过&连接
-
- POST的参数一般是通过params 携带参数
- 参数长度限制不同
-
- GET传送的数据量较小,一般不大于2KB
-
- POST传送的数据量较大,一般默认不受限制
-
- 解释:HTTP协议未规定GET和POST的长度限制,GET的最大长度是因为浏览器和web服务器限制了URL的长度,因为浏览器和web服务器处理长URL需要消耗比较多的资源,为了性能和安全(防止恶意构造长URL来攻击)考虑,会给URL长度加限制,不同的浏览器和web服务器限制的最大长度不一样。
- 缓存机制不同
- GET请求会被缓存
- POST请求一般不被缓存
13、手写
Ajax
介绍
AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)
如何使用ajax?
- 创建XMLHttpRequest对象
- 使用
open()方法创建http请求,并设置请求地址 - 设置发送的数据,使用
send()方法发送请求 - 注册
onreadystatechange事件
function XMLRequest(url) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr);
} else {
console.log(xhr.status);
}
}
};
}
// 使用
XMLRequest(url);
ajax+Promise的使用
function XMLRquest(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.status);
}
}
};
});
}
// 使用
XMLRquest(url)
.then((res) => {
let result = JSON.parse(res);
console.log(result);
})
.catch((err) => {
console.log(err);
});
new操作符
Javascript的new操作符做了哪些操作?
- 创建一个空对象
- 将这个空对象的原型,指向构造函数的原型
- 将空对象作为构造函数的上下文(即改变this指向)
- 对构造函数有返回值的处理判断
function create(fn, ...args) {
// 1、创建一个空对象
const obj = {}
// 2、将这空对象的原型,指向构造函数的原型
Object.setPrototypeOf(obj, fn.prototype)
// 3、将这个空对象,作为构造函数的上下文,即改变this指向
let res = fn.apply(obj, args)
// 4、对构造函数有返回值的处理判断
return res instanceof Object ? res : obj
}
使用
let obj = create(Person, 'kyrene', 24)
console.log(obj)
instanceof
function newInstanceOf(leftValue, rightValue) {
if (typeof leftValue !== 'object' || rightValue == null) {
return false
}
let rightProto = rightValue.prototype
let leftProto = leftValue.__proto__
while (true) {
if (leftProto == null) return false
if (leftProto == rightProto) return true
leftProto = leftProto.__proto__
}
}
验证
const a = []
const b = {}
function Foo() {}
var c = new Foo()
function Child() {}
function Father() {}
Child.prototype = new Father()
var d = new Child()
console.log(newInstanceOf(a, Array)) // true
console.log(newInstanceOf(b, Object)) // true
console.log(newInstanceOf(b, Array)) // false
console.log(newInstanceOf(a, Object)) // true
console.log(newInstanceOf(c, Foo)) // true
console.log(newInstanceOf(d, Child)) // true
console.log(newInstanceOf(d, Father)) // true
console.log(newInstanceOf(123, Object)) // false
console.log(123 instanceof Object) // false
console.log(new Number(123) instanceof Object) // true
事件总线
class EventEmitter {
constructor() {
this.cache = {};
}
on(name, fn) {
/*
绑定事件:
1、先检查cache里面是否有该事件名
2、若有该事件名,则在事件数组中加入当先前事件
3、若无该事件名,则新建一个事件数组,并将当前事件加入事件数组中
*/
if (this.cache[name]) {
this.cache[name].push(fn);
} else {
this.cache[name] = [fn];
}
}
off(name, fn) {
/*
解绑事件:
1、判断当前事件是否存在
2、若存在
则判断当前事件是否存在,若存在,就把当前事件从事件数组中移除
3、若不存在,则什么也不做
*/
let task = this.cache[name];
if (task) {
let index = task.indexOf(fn);
if (index != -1) {
task.splice(index, 1);
}
}
}
emit(name, once = false, ...args) {
/*
触发事件:
1、判断当时事件是否存在
2、若存在
执行这个事件名对应事件数组中的所有事件
判断事件是否存在只执行一次的参数选项,若是,执行完之后就删除该事件
3、若不存在,则什么也不做
*/
if (this.cache[name]) {
let tasks = this.cache[name].slice();
for (let fn of tasks) {
fn(...args);
}
if (once) {
delete this.cache[name];
}
}
}
}
三种排序
冒泡排序
const bubbleSort = function (nums) {
const length = nums.length - 1
for (let i = 0; i < length; i++) {
for (let j = 0; j < length - i; j++) {
if (nums[j] > nums[j + 1]) {
let temp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = temp
}
}
}
return nums
}
let resBubbleSort = bubbleSort([5, 2, 4, 7, 9, 8, 3, 6, 3, 8, 3])
console.log(resBubbleSort)
快速排序
const quickSort = function (nums) {
if (nums.length < 2) {
return nums
} else {
var left = []
var right = []
let pivot = Math.floor(nums.length / 2)
var base = nums.splice(pivot, 1)[0]
for (let i = 0; i < nums.length; i++) {
if (nums[i] < base) {
left.push(nums[i])
} else {
right.push(nums[i])
}
}
}
return quickSort(left).concat([base], quickSort(right))
}
let resQuickSort = quickSort([1, 34, 5, 76, 8, 6, 9, 7, 6, 3])
console.log(resQuickSort)
选择排序
const selectSort = function (nums) {
for (let i = 0; i < nums.length; i++) {
let index = i
for (let j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[index]) {
index = j
}
}
if (nums[i] > nums[index]) {
let temp = nums[index]
nums[index] = nums[i]
nums[i] = temp
}
}
return nums
}
let resSelectSort = selectSort([6, 45, 3, 2, 5, 6, 8, 4, 3, 4, 56, 67, 5])
console.log(resSelectSort);