1.JS中的变量
(1)变量提升和函数提升
变量提升和函数提升的过程:
- 创建阶段:JS解释器会找出需要提升的变量和函数,并给他们在内存中开辟好空间,变量的话会声明并且赋值为undefined,而函数会整个存储在内存中
- 代码执行阶段:就可以使用上面进行提升的变量和函数了
注意:let也会提升,但是let存在暂时性死区,所以在声明之前不可使用
(2)let,var,const区别
参考这篇文章:let与var的区别
let定义的变量不会挂载到window对象上,而是形成一个块作用域,而var定义的变量会挂在到window上
2.apply,call,bind
(1)apply,call,bind作用
(2)apply,call,bind区别
- apply接收的是一个数组类型的参数,call和bind接收的是一个参数列表
- apply和call返回的是一个值结果,而bind返回的是一个函数,需要再次进行调用
方法 |
语法 |
参数 |
返回值 |
apply |
func.apply(thisArg, [argsArray]) |
this,数组或类数组对象 |
结果值 |
call |
function.call(thisArg, arg1, arg2, …) |
this,参数列表 |
结果值 |
bind |
function.bind(thisArg, arg1, arg2, …) |
this,参数列表 |
函数 |
bind()方法返回的函数没有prototype属性,为什么没有prototype属性呢,因为如果把这个返回的函数做构造函数的话,那就需要将创建对象的this指向这个函数,但是这个函数在bind调用前就将this指向调用对象了,那么创建对象时又将这个this指向创建对象,不就矛盾了吗,所以bind()返回的函数没有prototype属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
var obj = {
age: 25
}
function getInfo(job, height) {
console.log(this);
this.d = 50;
}
Function.prototype.myBind = function () {
if (typeof this !== 'function') {
throw new TypeError('调用者必须是函数')
}
var arg = arguments[0],
slice = Array.prototype.slice,
args = slice.call(arguments, 1),
_that = this;
return function () {
return _that.apply(arg, args.concat(slice.call(arguments)));
}
}
var c = new getInfo.myBind(obj, 'Software Engineer'); // Uncaught TypeError: 调用者必须是函数
|
如果bind方法作为构造函数的话,那么它里面的this就需要指向创建的对象,而bind中的this必须指向函数,所以此时就会报错。
apply,call,bind用法展示:
1
2
3
4
5
6
7
8
9
10
11
12
|
let obj = {
age: 18,
job: 'student'
}
function getInfo(name) {
console.log(name); // 小花
console.log(this.age); // 18
console.log(this.job); // student
}
// getInfo.apply(obj, ['小花']);
// getInfo.call(obj, '小花');
getInfo.bind(obj, '小花')();
|
1
2
3
4
5
6
7
8
9
|
let obj = {
age: 18
}
function getInfo(job, height) {
console.log(job);
console.log(height);
console.log(this.age);
}
getInfo.bind(obj, 'student')(173);
|
(3)手写apply()
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
|
let obj = {
name: '小王'
}
function getInfo(job) {
console.log(this.name);
console.log(job);
}
// getInfo.apply();
Function.prototype.myApply = function (context) {
context = context || window;
context.fn = this;
let result;
if (arguments[1]) {
if (Array.isArray(arguments[1])) {
result = context.fn(...arguments[1]);
} else {
console.error('Uncaught TypeError: CreateListFromArrayLike called on non-object');
}
} else {
result = context.fn()
}
delete context.fn;
return result;
}
getInfo.myApply(obj, ['student']);
|
(4)手写call()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
let obj = {
age: 18
}
function getInfo(job, name) {
console.log(job);
console.log(name);
console.log(this.age);
}
// getInfo.call(obj, 'student');
Function.prototype.myCall = function (context) {
context = context || window;
context.fn = this;
let result;
if (arguments[1]) {
result = context.fn(...[...arguments].slice(1));
} else {
result = context.fn();
}
delete context.fn;
return result;
}
getInfo.myCall(obj, 'student', '小王');
|
(5)bind()
1
2
3
4
5
6
7
8
9
|
let obj = {
age: 18
}
function getInfo(job, height) {
console.log(job);
console.log(height);
console.log(this.age);
}
getInfo.bind(obj, 'student')(173);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return this.x + ',' + this.y;
}
let p = new Point(1, 2);
p.toString();
let emptyObj = {};
let YAxisPoint = Point.bind(emptyObj, 0);
let axisPoint = new YAxisPoint(5);
axisPoint.toString();
|
第一种😎😎:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
if (!Function.prototype.bind) (function () {
var slice = Array.prototype.slice;
Function.prototype.bind = function () {
var thatFunc = this, thatArg = arguments[0];
var args = slice.call(arguments, 1);
if (typeof thatFunc !== 'function') {
throw new TypeError('Function.prototype.bind-' +
'what is trying to be bound is not callable');
}
return function () {
var funcArgs = args.concat(slice.call(arguments));
return thatFunc.apply(thatArg, funcArgs);
}
}
})()
|
仿写这种实现bind():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
var obj = {
age: 25
}
function getInfo(name, job) {
console.log(name);
console.log(job);
console.log(this.age);
}
// getInfo.bind(obj, '星空海绵')('Software Engineer');
Function.prototype.myBind = function () {
if (typeof this !== 'function') {
throw new TypeError('调用者必须是函数')
}
var arg = arguments[0];
var slice = Array.prototype.slice
var args = slice.call(arguments, 1);
var _that = this;
return function () {
return _that.apply(arg, args.concat(slice.call(arguments)))
}
}
getInfo.myBind(obj, '星空海绵')('Software Engineer');
|
上面定义变量为什么要用var呢,因为用的是ES5,如果用let,都有ES6了,那我就直接用bind()了,而不是手写一个了。
我们会发现使用上面这种手写bind()不支持new调用构造函数创建新的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
console.log('toString方法');
}
Function.prototype.myBind = function () {
if (typeof this !== 'function') {
throw new TypeError('调用者必须是函数')
}
var arg = arguments[0];
var slice = Array.prototype.slice
var args = slice.call(arguments, 1);
var _that = this;
return function () {
return _that.apply(arg, args.concat(slice.call(arguments)))
}
}
let YAxisPoint = Point.myBind({}, 0);
let axisPoint = new YAxisPoint(5);
console.dir(axisPoint);
axisPoint.toString();
|
看下上面这个代码的axisPoint是啥
我们再看如果把上面代码中的myBind换成bind
也就是说上面这种写法不支持new调用构造函数创建新的对象,所以说有了下面第二种方法。
注意,当使用new操作符绑定构造函数时,会忽略第一个参数
1
2
3
4
5
6
7
|
var obj = {
age: 25
}
function getInfo(job, height) {
console.log(this); // {age: 25}
}
getInfo.bind(obj, 'Software Engineer')();
|
以上是没有用new调用的时候,getInfo中的this是指向obj的,没问题
1
2
3
4
5
6
|
function A() {
this.a = 100;
console.log(this); // {a: 100}
}
let b = new A();
console.log(b); // {a: 100}
|
1
2
3
4
5
6
7
8
|
var obj = {
age: 25
}
function getInfo(job, height) {
console.log(this); // getInfo {}
}
var a = getInfo.bind(obj, 'Software Engineer');
var b = new a();
|
以上的代码你会发现getInfo中的this并不是obj,而是getInfo对象,上面我们也看到有说,当使用new操作符号时会忽略掉第一个参数,也就是让调用bind的函数中的this指向的第一个参数被忽略掉,然后我们再想想bind的作用,不就是改变函数得this指向吗,现在第一个参数都忽略了,那么bind是不是相当于没用了,仔细想想,确实可以这个理解。
1
2
3
4
5
6
7
8
|
var obj = {
age: 25
}
function getInfo(job, height) {
console.log(this); // getInfo {}
}
var a = getInfo.bind(obj, 'Software Engineer');
var b = new a();
|
1
2
3
4
5
6
7
8
|
var obj = {
age: 25
}
function getInfo(job, height) {
console.log(this); // getInfo {}
}
var b = new getInfo('Software Engineer');
console.log(b);
|
相对而言,上面这两段代码其实通过构造函数创建的b对象时一样的,也就是说当使用了new操作符号时,bind就没用,就是单纯的返回一个函数。理解了这个我们就会明白为什么之前的代码会存在不知new调用构造函数创建新对象了,那么就有了第二种方法。
当使用new调用构造函数时,构造函数中的this是指向新创建的对象的
第二种😯😯:
使用ES6的new.target加以理解
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
|
var obj = {
age: 25
}
function getInfo(job, height) {
console.log(this);
this.d = 50;
}
Function.prototype.myBind = function () {
if (typeof this !== 'function') {
throw new TypeError('调用者必须是函数')
}
var arg = arguments[0],
slice = Array.prototype.slice,
args = slice.call(arguments, 1),
_that = this;
var fn1 = {};
fn1 = new getInfo();
return function () {
if (new.target) {
return fn1
} else {
return _that.apply(arg, args.concat(slice.call(arguments)));
}
}
}
var c = getInfo.myBind(obj, 'Software Engineer');
var d = new c();
console.log(d);
|
3.原型链
(1)对象和函数的原型
每个对象都有一个原型,但是原型没有提供可以直接访问的属性,所以浏览器实现了一个__proto__属性,可以直接访问到对象的原型,每个函数都有一个__proto__和prototype属性,当函数当对象使用时,原型是__proto__,当函数作为构造函数使用时,原型是prototype。
1
2
3
4
5
6
7
8
9
|
function Demo() { }
Demo.__proto__.show = function () {
console.log("__proto__show");
}
Demo.prototype.read = function () {
console.log("prototype-read");
}
Demo.show(); // __proto__show
Demo.read(); // Demo.read is not a function
|
1
2
3
4
5
6
7
8
9
10
|
function Demo() { }
let obj = new Demo();
Demo.prototype.read = function () {
console.log("read");
}
Demo.__proto__.show = function () {
console.log("show");
}
obj.read(); // read
obj.show(); // obj.show is not a function
|
(2)原型链
对象可以通过__proto__属性找到不属于该对象的属性,__proto__将对象连接起来组成了原型链。
1
2
3
4
5
6
7
8
9
10
|
function A() { }
function B() { }
function C() { }
C.prototype.show = function () {
console.log('C构造函数');
}
B.prototype = new C();
A.prototype = new B();
let a = new A();
a.show(); // C构造函数
|
4.如何判断对象类型
(1)通过 Object.prototype.toString.call()
1
2
3
4
5
6
|
let obj = {
age: 20
}
let arr = [15, 20, 25];
console.log(Object.prototype.toString.call(obj)); // [object Object]
console.log(Object.prototype.toString.call(arr)); // [object Array]
|
(2)通过instanceof
instanceof检测构造函数的prototype属性是不是出现在某个对象的原型链上
1
2
3
4
5
6
|
function A() { };
function B() { };
const b = new B();
A.prototype = b;
const a = new A();
console.dir(a instanceof A); // true
|
5.箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var age = 10;
let obj = {
age: 20,
say: () => {
return this.age;
}
}
console.log(obj.say()); // 10
let obj1 = {
age: 30
}
obj1 = Object.create(obj);
obj1.age = 30;
console.log(obj1.say()); // 10
|
通过上面的代码我们会发现箭头函数say中的this并不会因为调用对象的改变而改变,而是一直指向全局对象window的。
1
2
3
4
5
6
7
8
9
10
11
|
function getName() {
let say = () => {
console.log(this.age);
}
say();
}
let obj = {
age: 20,
getName: getName
}
obj.getName(); // 20
|
箭头函数没有自己的this,它只会从自己作用域链的上层继承this。
箭头函数中不能使用arguments和super以及new.target,同时箭头函数也不能作为构造函数。
6.this
(1)谁调用函数,函数里得this就指向谁
1
2
3
4
5
6
7
8
9
10
11
|
function getAge() {
console.log(this.age)
}
var age = 1
getAge()
var obj = {
age: 2,
getAge: getAge
}
obj.getAge()
|
(2)new构造函数创建对象时,构造函数内部的this指向创建的对象
(3)可以通过apply,call,bind来改变函数内部的this指向
7.async和await
(1)async和await对比Promise
async和await相比Promise,优势在于处理then调用链,代码更清晰,缺点是乱用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);
a = (await 10) + a;
console.log('3', a);
}
b();
a++;
console.log('1', a);
|
待深入
8.generator
(1)generator是ES6中新增的语法,和Promise一样,都可以用来进行异步编程
1
2
3
4
5
6
7
8
9
|
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next());
console.log(b.next());
console.log(b.next());
|
(2)generator原理,手写generator
9.Promise
(1)Promise是ES6中新增的语法,用来解决回调地狱问题
(2)手写Promise
10. == 和 === 的区别
==只需要值相等即可,而 === 需要值和类型都相等
11. 垃圾回收
主要的两种标记策略:标记清理 和 引用计数
(1)标记清理
JS最常用的垃圾回收策略就是标记清理。
标记方法可以有多种:
- 可以当变量进入上下文中,反转某一位
- 也可以维护“在上下文中”和“不在上下文中”这样的两个变量列表,可以把变量从一个列表移到另一个列表
- 其它
垃圾回收程序运行时,会标记内存中存储的所有变量,然后,它会将所有上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是有待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
(2)引用计数
另一种没那么常用的策略就是引用计数。
引用计数存在解决不了循环引用的问题
12.闭包
(1)定义
函数A返回函数B,函数B中使用了函数A中的变量,函数B就被称为闭包。
1
2
3
4
5
6
7
|
function A() {
let age = 10;
return function B() {
console.log(age);
}
}
A()();
|
上面这个例子age变量是存储在堆上的,因为JS的引擎可以分析出哪些变量需要存储在堆上,哪些变量需要存储在栈上。
通过闭包解决变量作用域的问题,例如:解决var没有块作用域的问题
1
2
3
4
5
6
7
8
|
for (var i = 0; i < 4; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
})
}
)(i)
}
|
(2)闭包的内存泄漏问题
1
2
3
4
5
6
|
let outer = function () {
let name = 'Jake';
return function () {
return name;
}
}
|
上面这段代码调用outer()会导致分配给name的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理name,因为闭包一直在引用着它,假如name的内容很大,那可能就是个大问题。
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
13.基本类型和引用类型
基础类型存储在栈上,引用类型的地址存储在栈上,内容存储在堆上
14.Eventloop
(1)为什么JS是单线程的
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这就决定了它只能是单线程,否则的话就会出现很多复杂的同步问题。假设JS有两个线程,一个线程往某个dom节点上添加内容,一个线程删除这个节点,那么应该以哪个线程为主呢,所以JS是单线程的就不会出现这种情况。
JS并发事件模型
(1)什么是Eventloop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
(2)微任务和宏任务
15.回调函数
16.setTimeout
(1)setTimeout倒计时误差
为什么会出现这个问题呢,从刚才上面的JS事件循环机制我们就可以了解到,只有等同步任务全部执行完了才会执行异步任务,而setTimeout是一个宏任务,那么这个问题的实际应用在哪呢,电商的秒杀倒计时,那个肯定要准确,不同用户可能因为手机的性能加载页面的时长不一样,但是秒杀时间肯定得一样,所以说我们就需要写一个相对准确的倒计时了。
1
|
setTimeout(() => {}, 3000);
|
有一行上面这样的代码,这不是说页面打开3秒后立即执行回调函数,而是页面打开后最快3秒执行回调函数。
1
2
3
4
5
6
|
console.log(Date());
setTimeout(() => {
console.log('我是定时器!');
console.log(Date());
}, 3000);
for (let i = 0; i < 10000000000; i++) { }
|
如果你电脑性能好的话可以跑下上面这个例子,你会发现间隔时间并不是3秒
看我这打印的结果,中间差了14秒,这是为什么呢,因为执行前面的循环花费了很长的时间,setTimeout必须等循环执行完才会去执行自己的回调函数,所以这时候这个倒计时就不准确了,就需要我们自己写一个倒计时
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
|
let period = 60 * 1000 * 60 * 2,
startTime = new Date().getTime(),
count = 0,
end = new Date().getTime() + period,
interval = 1000,
currentInterval = interval;
function loop() {
count++;
let offset = new Date().getTime() - (startTime + count * interval),
diff = end - new Date().getTime(),
h = Math.floor(diff / (60 * 1000 * 60)),
hdiff = diff % (60 * 1000 * 60),
m = Math.floor(hdiff / (60 * 1000)),
mdiff = hdiff % (60 * 1000),
s = mdiff / (1000),
sCeil = Math.ceil(s),
sFloor = Math.floor(s),
currentInterval = interval - offset;
console.log('时:' + h, '分:' + m, '毫秒:' + s,
'秒向上取整:' + sCeil, '代码执行时间:' + offset,
'下次循环间隔' + currentInterval)
// 打印 时 分 秒 代码执行时间 下次循环间隔
setTimeout(loop, currentInterval)
}
loop();
|
17.防抖节流
18.数组降维
MDN参考
(1)使用 ES10 中的 flat()
1
2
3
4
|
let arr = [50, 20, [50, 10, [1, [5, [20]]]]],
arr1 = arr.flat(Infinity);
console.log(arr); // [50, 20, Array(3)]
console.log(arr1); // [50, 20, 50, 10, 1, 5, 20]
|
(2)递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
let arr = [50, 20, [50, 10, [1, [5, [20]]]]],
resArray = [];
function flatten(array) {
if (Array.isArray(array) === false) {
throw new TypeError('参数类型错误');
}
array.forEach(item => {
if (Array.isArray(item)) {
flatten(item);
} else {
resArray.push(item)
}
})
return resArray;
}
console.log(arr); // [50, 20, Array(3)]
console.log(flatten(arr)); // [50, 20, 50, 10, 1, 5, 20]
|
(3)