web前端进阶篇(一 )JS

发布时间:2025-05-24 17:31:26 作者:益华网络 来源:undefined 浏览量(1) 点赞(1)
摘要:1谈谈变量提升 当执行JS代码时,会生成执行环境,只要代码不是写在函数中的,就是在堆栈执行环境中,函数中的代码会产生函数执行环境,仅此两种执行环境。

1谈谈变量提升

当执行JS代码时,会生成执行环境,只要代码不是写在函数中的,就是在堆栈执行环境中,函数中的代码会产生函数执行环境,仅此两种执行环境。

1
2
3
4
5
6
b() // call b
console.log(a) // undefined
var a = Hello world
function b() {
console.log(call b)
}

想必高于上述的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS解释器会寻找需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明和赋值undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

1
2
3
4
5
6
7
8
b() // call b second
function b() {
console.log(call b fist)
}
function b() {
console.log(call b second)
}
var b = Hello world

var会产生很多错误,所以在ES6中约会了let。let不能在声明前使用,但是这不是常说的let不会提升,提升let了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用

2绑定,调用,应用区别

call和apply都是为了解决改变this的指向。作用都是相同的,只是传参的方式不同。

除了第一个参数外,call可以接收一个参数列表,apply只接受一个参数列表

1
2
3
4
5
6
7
8
9
10
let a = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(a, yck, 24)
getValue.apply(a, [yck, 24])

bind和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过

bind实现柯里化

3如何实现一个bind函数

对于实现以下几个函数,可以从几个方面思考

不预定第一个参数,那么预设为 window

改变this思路指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myBind = function (context) {
if (typeof this !== function) {
throw new TypeError(Error)
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}

4如何实现一个call函数

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.myCall = function (context) {
var context = context || window
// 给 context 添加一个属性
// getValue.call(a, yck, 24) => a.fn = getValue
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a, yck, 24) => a.fn(yck, 24)
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}

5如何实现一个apply函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this
var result
// 需要判断是否存储第二个参数
// 如果存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}

6简单说下原型链?

每个函数都有prototype属性,除了Function.prototype.bind(),该属性指向原型。

每个对象都有__proto__属性,指向了创建该对象的构造函数的原型。其实这个属性指向了[[prototype]],但是[[prototype]]是内部属性,我们并不能访问到,所以使用_proto_来访问。

对象可以通过__proto__来寻找不属于该对象的属性,__proto__将对象连接起来组成了原型链。

7怎么判断对象类型

可以通过Object.prototype.toString.call(xx)。这样我们就可以获得类似[object Type]的字符串。

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

8箭头函数的特点

1
2
3
4
5
6
7
8
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())

函数箭头的英文其实没有this的,函数这个中的this只取决于他外面的第一个不是箭头函数的函数的this。在这个例子中,调用因为a符合前面代码中的第一个情况,所以this的英文window。并且this一旦绑定了一部分,就不会被任何代码改变

9这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<h2>function foo() {
<br>console.log(this.a)
<br>}
<br>var a = 1
<br>foo()
<br>var obj = {
<br>a: 2,
<br>foo: foo
<br>}
<br>obj.foo()
<br>// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况
<br>// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
<br>var c = new foo()
<br>c.a = 3
<br>console.log(c.a)
<br>// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
<br></h2>

10 async,await优缺点

async和await在于直接使用而言Promise,优势在于处理then的调用链,能够更清晰准确的写出代码。一致在于重复await可能会导致性能问题,因为await会分离代码,也许之后的编码并不依赖于前者,但仍然需要等待前者完成,导致代码丢失了并发性

下面来看一个使用await的代码。

1
2
3
4
5
6
7
8
9
10
var a = 0
var b = async () => {
a = a + await 10
console.log(2, a) // -> 2 10
a = (await 10) + a
console.log(3, a) // -> 3 20
}
b()
a++
console.log(1, a) // -> 1 1

首先函数b先执行,在执行到await 10之前变量a还是0,因为在await内部实现了generators,generators会保留多个中东西,所以这时候a = 0被保存了下来

因为await是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,导致函数外部的代码可以继续执行,所以会先执行console.log(‘1’, a)

这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10

然后后面就是常规执行代码了

11 generator原理

Generator是ES6中新增的语法,和Promise一样,都可以用来初始化编程

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

从以上代码可以发现,加上*的函数执行后拥有了next函数,然后函数执行后返回了一个对象。每次调用next函数可以继续执行被暂停的代码。以下是Generator函数的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};
return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}

12承诺

Promise是ES6补充的语法,解决了备选地狱的问题。

可以把Promise看成一个状态机。初始是pending状态,可以通过函数resolve和reject,将状态转换为resolved或者rejected状态,状态一旦改变就不能再次变化。

then函数会返回一个Promise实例,并且该返回值是一个新的实例而不是之前的实例。因为Promise规范规定pending已有状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个then调用就失去了意义了。对于then来说,本质上可以把它看成是flatMap

13如何实现一个承诺

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调,只有当 promise
// 状态为 pending 时才会缓存,并且每个实例至多缓存一个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];
_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 如果 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};
_this.reject = function (reason) {
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用于解决以下问题
// new Promise(() => throw Error(error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}
MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7,then 必须返回一个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 如果类型不是函数需要忽略,同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === function ? onResolved : v => v;
onRejected = typeof onRejected === function ? onRejected : r => throw r;
if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
// 所以用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,所以使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,如果是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中一个执行过得话,忽略其他的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,如果不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 如果 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}

14 和=区别,什么情况用==

这里来解析一道[] == ![] // -> true译文,下面是这个表达式为何为true的步骤

1
2
3
4
5
6
7
8
9
10
11
12
// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> 
 == 0
// 根据第 6 条得出
0 == 0 // -> true
===在开发中,对于预先返回的code,可以通过==去判断

15基本数据类型和引荐类型在存储上的差异

前者存储在栈上,另外存储在堆上

16浏览器Eventloop和Node中的有什么区别

如果JS是门多线程的语言话,我们在多个线程中处理DOM就可能会发生问题(一个线程),因为JS是门非双向单线程语言,因为在最初是JS就是为了和浏览器交互而产生的。中新加二进制,另一个线程中删除例程),当然可以约会读写锁解决这个问题。

JS在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到Task(有多种task)少量中。一旦执行栈为空,则将会Event Loop从Task中间中拿出需要执行的代码并加入执行栈中执行,所以本质上来说JS中的异步还是同步行为

1
2
3
4
5
console.log(script start);
setTimeout(function() {
console.log(setTimeout);
}, 0);
console.log(script end);

代码以上虽然setTimeout延时为0,其实还是异步。的英文这因为HTML5标准规定这个函数第二个参数不得小于4毫秒,不足会自动增加。所以setTimeout还是会在script end之后打印。

不同的任务源会被分配到不同的Task层次中,任务源可以分为微任务(microtask)和宏任务(macrotask)。在ES6规范中,microtask称为工作,macrotask称为task。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(script start);
setTimeout(function() {
console.log(setTimeout);
}, 0);
new Promise((resolve) => {
console.log(Promise)
resolve()
}).then(function() {
console.log(promise1);
}).then(function() {
console.log(promise2);
});
console.log(script end);
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然setTimeout写在Promise之前,但是因为Promise属于微任务而setTimeout属于宏任务,所以会有以上的打印。

微任务包括 process.nextTick,promise,Object.observe,MutationObserver

宏任务包括 script,setTimeout,setInterval,setImmediate,I/O,UI renderin

很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了script,浏览器会先执行一个宏任务,然后有异步代码的话就先执行微任务

所以正确的一次Event loop顺序是这样的

执行同步代码,这属于宏任务

执行栈为空,查询是否有微任务需要执行

执行所有微任务

必要的话渲染 UI

然后开始下一轮Event loop,执行宏任务中的异步代码

通过上述的 Event loop顺序可知,如果宏任务中的异步代码有大量的计算和需要操作DOM的话,为了更换的界面响应,我们可以把操作DOM放入微任务中

17 setTimeout倒计时误差

JS是单线程的,所以setTimeout的误差实际上是无法被完全解决的,原因有很多,可能是某些中的,有可能是浏览器中的各种事件导致。这也是为什么页面开久了,定时器会不准的原因,当然我们可以通过一定的办法去减少这个误差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 以下是一个相对准备的倒计时实现
var period = 60 * 1000 * 60 * 2
var startTime = new Date().getTime();
var count = 0
var end = new Date().getTime() + period
var interval = 1000
var currentInterval = interval
function loop() {
count++
var offset = new Date().getTime() - (startTime + count * interval); // 代码执行所消耗的时间
var diff = end - new Date().getTime()
var h = Math.floor(diff / (60 * 1000 * 60))
var hdiff = diff % (60 * 1000 * 60)
var m = Math.floor(hdiff / (60 * 1000))
var mdiff = hdiff % (60 * 1000)
var s = mdiff / (1000)
var sCeil = Math.ceil(s)
var sFloor = Math.floor(s)
currentInterval = interval - offset // 得到下一次循环所消耗的时间
console.log(时:+h, 分:+m, 毫秒:+s, 秒向上取整:+sCeil, 代码执行时间:+offset, 下次循环间隔+currentInterval) // 打印 时 分 秒 代码执行时间 下次循环间隔
setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

18片段降维

1
2
[1, [2], 3].flatMap(v => v)
// -> [1, 2, 3]

如果想将一个多维整数彻底的降维,可以这样实现

1
2
3
4
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]
flattenDeep([1, [[2], [3, [4]], 5]])

19深拷贝

这个问题通常可以通过JSON.parse(JSON.stringify(object))来解决

1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: FE
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = native
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

会忽略 undefined

会忽略 symbol

不能序列化函数

不能解决循环引用的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

在遇到函数,undefined或者symbol的时候,该对象也不能正常的序列化

1
2
3
4
5
6
7
8
let a = {
age: undefined,
sex: Symbol(male),
jobs: function() {},
name: yck
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能移动的。当然如果你的数据中含有以上某种情况下,可以使用lodash的深拷贝函数

20 typeof于instanceof区别

typeof对于基本类型,除了null都可以显示正确的类型

1
2
3
4
5
6
typeof 1 // number
typeof 1 // string
typeof undefined // undefined
typeof true // boolean
typeof Symbol() // symbol
typeof b // b 没有声明,但是还会显示 undefined

typeof 对于对象,除了函数都会显示 object

1
2
3
typeof [] // object
typeof {} // object
typeof console.log // function

对于null来说,虽然它是基本类型,但是会显示object,这是一个存在很久了的Bug

1
typeof null // object

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们也可以试着实现一下 instanceof
function instanceof(left, right) {
// 获得类型的原型
let prototype = right.prototype
// 获得对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}

二维码

扫一扫,关注我们

声明:本文由【益华网络】编辑上传发布,转载此文章须经作者同意,并请附上出处【益华网络】及本页链接。如内容、图片有任何版权问题,请联系我们进行处理。

感兴趣吗?

欢迎联系我们,我们愿意为您解答任何有关网站疑难问题!

您身边的【网站建设专家】

搜索千万次不如咨询1次

主营项目:网站建设,手机网站,响应式网站,SEO优化,小程序开发,公众号系统,软件开发等

立即咨询 15368564009
在线客服
嘿,我来帮您!