feat: 新功能(feature)

fix: 修补bug

docs: 文档(documentation)

style: 格式(不影响代码运行的变动)

refactor: 重构(即不是新增功能,也不是修改bug的代码变动)

chore: 构建过程或辅助工具的变动

revert: 撤销,版本回退

perf: 性能优化

test:测试

improvement: 改进

build: 打包

ci: 持续集成

《你不知道的JavaScript》系列二——–this绑定详解

关于this的误解

1.指向自身

不管是JavaScript新手开发者还是之前已经接触过其他语言的开发者,都很容易将this理解为指向函数自身。下面简单的例子将证明this远远不止字面意思的这么简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(num){
console.log("foo:"+num);
//希望用count来记录foo被调用的次数
this.count++;
}
foo.count = 0;
for(let i=0;i<5;i++){
foo(i);
}
//foo:0
//foo:1
//foo:2
//foo:3
//foo:4

//见证奇迹的时候到了
console.log(foo.count);//0 ———— WTF!!?

从上面代码的输出可以看出从字面意思来理解this是错误的,那如果我增加的count属性和预期的不一样,那我增加的count哪去了?看看下图就一目了然了

image-20240310165509618

2.指向函数的作用域

需要明确的是,this在任何情况下都不指向函数的词法作用域。在JavaScript内部,作用域确实是与对象类似,可见的标识符都是它的属性,但是作用域“对象”无法通过JavaScript代码访问,它存在与JavaScript引擎内部————《你不知道的JavaScript》

1
2
3
4
5
6
7
8
function foo(){
var a=2;
this.bar();
}
function bar(){
console.log(this.a);
}
foo();//ReferenceError: a is not defined

这段代码试图使用this来联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里面的变量a。这是不可能实现的,我们不能使用this来引用一个词法作用域内部的东西。

每当你想要把词法作用域和this的查找混合使用时,一定要提醒自己,这是无法实现的

this到底是什么

当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。

调用栈:为了到达当前执行位置所调用的所有函数

调用位置:在当前正在执行的函数的前一个调用中

this的四条绑定规则

1.默认绑定

独立函数调用时使用默认绑定规则,绑定到全局对象(非严格模式)或者undefined(严格模式)

1
2
3
4
5
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2

分析上述代码:

①:在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,一次只能使用默认绑定,无法使用其他规则。
②:上述代码执行在非严格模式下,因此this绑定到了全局对象上
③:this.a被解析为全局变量a,打印2

Tips:只有在非严格模式下调用foo(),默认绑定才能绑定到全局对象

2.隐式绑定

当调用位置有上下文对象,或者说是否被某个对象拥有或者包含时,考虑隐式绑定。

这个说法非常抽象,具体还是需要通过代码来进行讲解:

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this.a);
}

var obj = {
a:2,
foo:foo
};

obj.foo(); // 2

在上述代码中,调用位置会使用obj上下文来引用函数,因此隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因此this.aobj.a是一样的

Tips:对象属性引用链中只有最顶层或者说最后一层会影响调用位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
console.log(this.a);
}

var obj2 = {
a:333,
foo:foo
};
var obj1 = {
a:222,
obj2:obj2
};

obj1.obj2.foo(); // 333
==隐式丢失==

隐式丢失是非常常见的this绑定问题,也就是说被隐式绑定的函数丢失绑定对象,应用默认绑定,将this绑定到全局对象(严格模式)或者undefined(非严格模式)上;

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;
var a = "糟糕,我是全局对象";
bar(); // "糟糕,我是全局对象"

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。值得注意的是,参数传递就是一种隐式赋值,因此在回调函数中,绑定的this也是全局对象。

1
2
3
4
5
6
7
8
9
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var a = "糟糕,我是全局对象";
setTimeout(obj.foo,1000); // "糟糕,我是全局对象"

3.显式绑定

使用call()、apply()直接指定this的绑定对象,称之为显式绑定。

显式绑定的两个应用:

(1)硬绑定

观察以下代码,无论之后如何调用函数bar,它总会手动在obj上调用foo

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
var bar = function(){
foo.call(obj);
}
bar(); // 2
setTimeout(bar,100); // 2
//硬绑定的bar不可能再修改它的this
bar.call(window); // 2

由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Funtion.prototype.bind,bind()会返回一个硬绑定的新函数,它会将参数设置为this的上下文。

(2)API调用的上下文

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常称之为“上下文”(context),作用和bind()一样,确保回调函数可以使用指定的this

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(el,this.id);
}
var obj = {
id : "awesome";
}

//调用foo的时候把this绑定到obj上
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome

4.new绑定

使用new来调用函数,会自动执行下面的操作:

  1. 创建一个全新的对象
  2. 这个新对象会被执行原型连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果这个函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
1
2
3
4
5
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

使用new来调用foo()时,我们会构建一个新对象并将它绑定到foo()调用的this上。

5.四条绑定规则的优先级

优先级:new绑定>显式绑定>隐式绑定>默认绑定

6.绑定例外

(1)被忽略的this

如果将null或者undefined作为this的绑定对象传入call、apply或者bind中时,这些值会被忽略,应用默认绑定规则

(2)间接引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
console.log(this.a);
}
var a = 2;
var o = {
a : 3,
foo : foo
};
var p = {
a : 4
};
o.foo(); // 3
p.foo = o.foo;
p.foo(); // 2

赋值表达式p.foo = o.foo;的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo。因此此处应用的是默认绑定

(3)箭头函数

箭头函数不适用this的四种绑定规则,而是根据外层作用域来决定this,并且箭头函数的绑定无法修改,new也不行!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
return (a) => {
//this继承自foo()
console.log(this.a);
}
}
var obj1 = {
a:2
}
var obj2 = {
a:3
}
var bar = foo.call(obj1);
bar.call(obj2); // 2

7.总结

  1. 函数是否在new中调用?如果是的话this绑定的是新创建的对象

    var bar = new foo();

  2. 函数是否通过call、apply显式绑定或者bind硬绑定?如果是的话,this绑定的是指定对象

    var bar = foo.call(obj2);

  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是上下文对象

    var bar = obj1.foo()

  4. 如果以上都不是的话,使用默认绑定。在严格模式下,绑定到undefined,非严格模式下,绑定到全局对象

    var bar = foo()

需要注意的是有些调用,特别是赋值,可能会在无意中使用默认绑定标准。并且箭头函数不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this。

8.课后习题

1.
1
2
3
4
5
6
7
8
9
10
const object = {
message: 'Hello, World!',

getMessage() {
const message = 'Hello, Earth!';
return this.message;
}
};

console.log(object.getMessage()); // ??
2.
1
2
3
4
5
6
7
8
9
10
11
12
function Pet(name) {
this.name = name;

this.getName = () => this.name;
}

const cat = new Pet('Fluffy');

console.log(cat.getName());

const { getName } = cat;
console.log(getName());
3.
1
2
3
4
5
6
7
8
9
const object = {
message: 'Hello, World!',

logMessage() {
console.log(this.message);
}
};

setTimeout(object.logMessage, 1000);
4.

如何调用logMessage函数,让它打印 "Hello, World!" ?

1
2
3
4
5
6
7
const object = {
message: 'Hello, World!'
};

function logMessage() {
console.log(this.message);
}
5.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const object = {
who: 'World',

greet() {
return `Hello, ${this.who}!`;
},

farewell: () => {
return `Goodbye, ${this.who}!`;
}
};

console.log(object.greet());
console.log(object.farewell());
6.
1
2
3
4
5
6
7
8
9
10
11
12
13
var length = 4;
function callback() {
console.log(this.length);
}

const object = {
length: 5,
method(callback) {
callback();
}
};

object.method(callback, 1, 2);
7.
1
2
3
4
5
6
7
8
9
10
11
12
13
var length = 4;
function callback() {
console.log(this.length);
}

const object = {
length: 5,
method() {
arguments[0]();
}
};

object.method(callback, 1, 2);
答案
  1. 'Hello, World!'

    隐式调用,此时的this绑定object上

  2. 'Fluffy''Fluffy'

    当函数作为构造函数new Pet('Fluffy')调用时,构造函数内部的this等于构造的对象

    Pet构造函数中的this.name = name表达式在构造的对象上创建name属性。

    this.getName = () => this.name在构造的对象上创建方法getName。而且由于使用了箭头函数,箭头函数内部的this值等于外部作用域的this值, 即 Pet

    调用cat.getName()以及getName()会返回表达式this.name,其计算结果为'Fluffy'

  3. undefined

    object.logMessage作为回调函数实际上引用的是logMessage函数本身,因此此时的logMessage()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。

  4. 显式绑定or硬绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    message: 'Hello, World!'
    };

    function logMessage() {
    console.log(this.message); // logs 'Hello, World!'
    }

    // Using func.call() method
    logMessage.call(object);

    // Using func.apply() method
    logMessage.apply(object);

    // Creating a bound function
    const boundLogMessage = logMessage.bind(object);
    boundLogMessage();
  5. 'Hello, World!''Goodbye, undefined!'

    当调用object.greet()时,在greet()方法内部,this值等于 object,因为greet是一个常规函数。因此object.greet()返回'Hello, World!'

    但是farewell()是一个箭头函数,箭头函数中的this值总是等于外部作用域中的this值。

    farewell()的外部作用域是全局作用域,它是全局对象。因此object.farewell()实际上返回'Goodbye, ${window.who}!',它的结果为'Goodbye, undefined!'

  6. 4

    callback()是在method()内部使用常规函数调用来调用的。由于在常规函数调用期间的this值等于全局对象,所以this.length结果为window.length

    第一个语句var length = 4,处于最外层的作用域,在全局对象 window 上创建一个属性length

  7. 3

最近重新翻读《你不知道的JavaScript》有感,进行系列记录,本文适合已经对闭包和模块有些许理解的同学阅读~

下面复习一下闭包

闭包的定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包;闭包使得函数即使是在当前词法作用域之外执行也可以继续访问定义时的词法作用域。

1.探讨模块与闭包之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function CoolModule(){
var something = "cool";
var another = [1,2,3];
function doSomething(){
console.log(something);
}
function doAnother{
console.log(another.join('!'));
}
return {
doSomething:doSomething,
doAnother:doAnother
}
}

观察以上代码我们可以发现:

  1. CoolModule()只是一个函数,必须调用才能创建一个模块实例,如果不进行调用,内部作用域和闭包都无法被创建
  2. CoolModule()返回一个对象,该对象含有对内部函数而不是内部数据变量的引用,内部数据变量保持隐藏且私有的状态,我们可以把这个返回的对象看作本质上是模块的公共API
  3. doSomething和doAnother函数具有涵盖模块实例内部作用域的闭包

综上所述,模块模式需要两个条件:

  1. 必须要有外部的封闭函数,该函数至少被调用一次(每次调用都会创建一个新的模块实例)
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态

2.现代模块机制

将上述的模块定义封装进一个友好API里,代码很优雅,我觉得很值得学习

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
var MyModules = (function Manager() {
var modules = {}
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
//遍历依赖队列,使用模块中的实际依赖进行重新赋值
deps[i] = modules[deps[i]]
}
//调用函数,并且使用依赖
modules[name] = impl.apply(impl, deps)
}
function get(name) {
return modules[name]
}
return {
define: define,
get: get,
}
})()
MyModules.define('bar', [], function () {
function hello(who) {
return 'helloooooo' + who
}
return { hello: hello }
})
MyModules.define('foo', [bar], function (bar) {
var hungry = 'hipoo'
function awesome() {
console.log(bar.hello(hungry).toUpperCase())
}
return {
awesome: awesome,
}
})

var bar = MyModules.get('bar')
var foo = MyModules.get('foo')
console.log(bar.hello('hipoo'))
foo.awesome()

直接看上面的代码可能会有点难以理解,下面我搭配debug跟大家一起看看~

image-20240308112101523

从上图可以看到调用了get函数同时也持有了get函数对原始定义作用域的引用(可以看到私有的变量modules及我们添加在上面的bar和foo)这就是闭包的效果,同时暴露公共API(defineget)得以对隐藏且私有的内部数据变量进行访问或修改。

其次,该段代码的核心modules[name] = impl.apply(impl, deps)可以做到使用模块中与传递进来的依赖队列同名的依赖模块传递到函数中,实现函数调用。

相信我,以上代码看着简单写起来可不简单哦~不仅要熟悉掌握闭包的使用还要熟悉模块模式该设计模式,所以建议大家认真看看,最好自己动手写写ヾ(•ω•`)o

虚拟列表赶紧用起来,轻轻松松解决超多重复DOM节点造成的卡顿~

以下有三种不同级别的虚拟列表,分别针对生成的重复DOM节点是固定高度、不同高度和动态变化高度~

1.基础段位:固定高度

tutieshi_640x594_13s

虚拟列表的原理其实就是以下几条:

①一个外层盒子提供滚动事件

②外层盒子中装的第一个是platform,一个空盒子,这个空盒子的高度是列表如果真实渲染应该有的高度,作用是为了撑开外层盒子,提供滚动条

②外层盒子中装的第二个是展示列表盒子,这个盒子中放置所有现在应该出现在页面上的列表项和前后缓冲区。该盒子采用绝对定位,top值根据滚动位置实时改变,让展示列表不论怎么滚动一直出现在页面上

④酌情给一些在页面展示之前之后的缓冲区,防止因为用户滚动过快而造成的空白

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
<template>
<div class="virtualBox" @scroll="scrollEvent" ref="virtualBox">
<div class="platform" :style="{ height: platformHeight + 'px' }">
</div>
<div class="trueBox" :style="{ top: top + 'px' }">
<div v-for="(key, value) in showData" class="itemBox" ref="itemBox">
<button>看起来{{ key }} 其实我是{{ value }}</button>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'WebFront',
data() {
return {
listData: [],//真实列表Data
count: 100,//真实列表项的个数,我这里为了展示手动赋值,真是使用直接获取Data长度即可
platformHeight:0//platform的高度
showData: [],//被展示的列表Data
startIndex: 0,//开始截取listData的Index
showNum: 1,//页面高度可以展示几个列表项
top: 0,//展示列表盒子绝对定位的top值
catchFrontNum: 4, //前缓冲区的数量
catchBackNum: 4,//后缓冲区的数量
itemHeight: 0,//列表项的高度
}
},
methods: {
scrollEvent(e) {
let scrollTop = e.target.scrollTop//获取滚动的距离
this.startIndex = Math.ceil(scrollTop / this.itemHeight)//滚动距离除以列表项的高度得到应该展示的列表项Index
this.startIndex =
this.startIndex < this.catchFrontNum
? 0
: this.startIndex - this.catchFrontNum//设置前缓冲区
//对展示的数组进行截取
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + this.catchBackNum + this.catchFrontNum
)
//绝对定位的展示列表项盒子的top值
this.top = this.startIndex * this.itemHeight
},
},
mounted() {
const virtualBox = this.$refs.virtualBox // 获取到最外层盒子
let itemBox = document.getElementsByClassName('itemBox')[0]
this.itemHeight = itemBox.offsetHeight//获取列表项
this.platformHeight = this.count * this.itemHeight
this.showNum = Math.ceil(virtualBox.clientHeight / this.itemHeight)//外层盒子的可视高度除以列表项高度可以得到展示数量
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + this.catchBackNum+ this.catchFrontNum
)
},
created() {
//做一些假数据用于展示
let i = 0
for (i = 0; i < 100; i++) {
this.listData[i] = '我是' + i
}
this.showData = this.listData.slice(0, 20)
},
}
</script>
<style scoped>
.trueBox {
position: absolute;
top: 0;
}
.itemBox {
height: 50px;
background-color: green;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
}
.platform {
background-color: red;
}
.virtualBox {
height: 85vh;
overflow: scroll;
position: relative;
}
</style>

2.进阶段位:不同高度

tutieshi_640x594_13s

与固定高度不同,列表项的高度是不固定的,所以会出现以下这些难点:

①无法通过页面高度除以列表项高度得到应当展示的数量,也就是展示列表的长度

②无法通过滚动了的高度scrollTop除以列表项高度得到此时应该展示的列表项Index

③无法直接通过ListData的长度乘以列表项高度得到platform的高度

对于以上难点我们的解决方案:

①设置一个预告高度,用于计算页面展示的数量,该预估高度建议偏小,避免出现页面展示数量不够的情况

②设置一个position数组,计算并存储每一个列表项的top\bottom\height值,通过比较scrollTop和列表项的position可以得到此时应该展示的列表项Index

③通过position数组获取最后一个列表项的bottom值,即为platform的高度

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
<template>
<div class="virtualBox" @scroll="scrollEvent" ref="virtualBox">
<div class="platform" :style="{ height: platformHeight + 'px' }">
<!-- 这是假的容器,作用:撑开盒子和提供滚动效果 -->
</div>
<div class="trueBox" :style="{ top: top + 'px' }">
<div
v-for="(item, key) in showData"
class="itemBox"
ref="items"
:id="item.id"
:key="item.id"
>
看着第{{ key }}个 其实第{{ item.id }}个
{{ item.value }}
</div>
</div>
</div>
</template>

<script>
export default {
name: 'WebFront',
data() {
return {
position: [],
listData: [],
platformHeight: 0,
count: 100,
scrollTop: 0,
showData: [],
startIndex: 0,
showNum: 0,
top: 0,
estimatedItemHeight: 100,//预设高度
}
},
methods: {
updateItemsSize() {
//更新列表项高度
let nodes = this.$refs.items
nodes.forEach((node) => {
let rect = node.getBoundingClientRect()
let height = rect.height
let index = parseInt(node.id)
let oldHeight = this.position[index].height
let dValue = oldHeight - height
if (dValue) {
this.position[index].bottom = this.position[index].bottom - dValue
this.position[index].height = height
for (let k = index + 1; k < this.position.length; k++) {
this.position[k].top = this.position[k - 1].bottom
this.position[k].bottom = this.position[k].bottom - dValue
}
this.platformHeight = this.position[this.position.length - 1].bottom
}
})
},
findStartIndex(scrollTop, list) {
//根据滚动高度scrollTop找到此时的startIndex
for (let i = 0, len = list.length; i < len; i++) {
if (list[i].top > scrollTop) {
return i - 1
}
}
return list.length - 1
},
scrollEvent(e) {
this.updateItemsSize()
this.scrollTop = e.target.scrollTop
let index = this.findStartIndex(this.scrollTop, this.position)
this.startIndex =
index < this.listData.length - 1 - this.showNum
? index
: this.listData.length - 1 - this.showNum
//至少保留showNum个列表项
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + 2
)
this.top = this.position[this.startIndex].top
},
createString(num) {
let str = ''
for (let i = 0; i < num; i++) {
str += 'aa'
}
return str
},
},
mounted() {
this.position = this.listData.map((item, index) => ({
index,
top: index * this.estimatedItemHeight,
bottom: (index + 1) * this.estimatedItemHeight,
height: this.estimatedItemHeight,
}))
this.platformHeight = this.position[this.position.length - 1].bottom
this.showNum = Math.ceil(
this.$refs.virtualBox.clientHeight / this.estimatedItemHeight
)
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + 2
)
},
created() {
let i = 0
for (i = 0; i < 100; i++) {
this.listData[i] = {}
this.listData[i].value = this.createString(
Math.floor(Math.random() * 100)
)
this.listData[i].id = i
}
this.showData = this.listData.slice(0, 20)
},
}
</script>
<style scoped>
.trueBox {
position: absolute;
top: 0;
}
.itemBox {
background-color: green;
display: block;
line-height: 100%;
word-break: break-all;
width: 100px;
padding: 10px;
border: 2px purple solid;
}
.platform {
background-color: red;
}
.virtualBox {
height: 85vh;
overflow: scroll;
position: relative;
}
</style>

3.高阶段位:变化高度

这种情况可能出现在比如列表项因为太长而设置了展开/收缩按钮,此时列表项的高度是动态发生变化的,这种情况和上一种情况差不多,区别只在于这种情况只需要在点击按钮的时候将position更新即可~所以在这里不做代码演示啦

总结

了解了原理要写出来还是不难的~但我个人感觉有的时候前端的进阶难就难在止步于此,现在的浏览器性能好,可能写N个DOM都不会卡顿,很难会有觉悟自己去写一个虚拟列表来试试看性能是不是更好。希望自己永不止步,永远进步

JavaScript中valueOf、toString的隐式调用

在你不知道的地方JS偷偷调用了valueOf和toString!

一、问题引入

你知道执行以下三种情况,控制台会打印什么吗?

(1)情况一:方法的返回值为1

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
Array.prototype.toString = function () {
console.log('array-----调用toString方法')
return 1
}
Array.prototype.valueOf = function () {
console.log('array-----调用valueOf方法')
return 1
}
Function.prototype.toString = function () {
console.log('function-----调用toString方法')
return 1
}
Function.prototype.valueOf = function () {
console.log('function-----调用valueOf方法')
return 1
}

let arr = new Array()
let fn = function () {
console.log('函数内打印')
}
console.log(arr)
console.log(fn)
console.log('console.log-----' + arr)
console.log('console.log-----' + fn)

(2)情况二:方法无返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Array.prototype.toString = function () {
console.log('array-----调用toString方法')
}
Array.prototype.valueOf = function () {
console.log('array-----调用valueOf方法')
}
Function.prototype.toString = function () {
console.log('function-----调用toString方法')
}
Function.prototype.valueOf = function () {
console.log('function-----调用valueOf方法')
}

let arr = new Array()
let fn = function () {
console.log('函数内打印')
}
console.log(arr)
console.log(fn)
console.log('console.log-----' + arr)
console.log('console.log-----' + fn)

(3)情况三:方法返回值为一个匿名函数

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
Array.prototype.toString = function () {
console.log('array-----调用toString方法')
return function () {}
}
Array.prototype.valueOf = function () {
console.log('array-----调用valueOf方法')
return function () {}
}
Function.prototype.toString = function () {
console.log('function-----调用toString方法')
return function () {}
}
Function.prototype.valueOf = function () {
console.log('function-----调用valueOf方法')
return function () {}
}

let arr = new Array()
let fn = function () {
console.log('函数内打印')
}
console.log(arr)
console.log(fn)
console.log('console.log-----' + arr)
console.log('console.log-----' + fn)

控制台输出如下,你答对了吗?

控制台输出结果:

image-20220809115757055.png

image-20220809115923874

image-20220809115757055.png

全对的大佬可以出门左转了,出现错误的同学请继续往下看~

二、具体情况分析

针对直接使用cosole.log打印单独的对象,打印语句一在三种情况下都发挥正常。但是往下看,打印语句二console.log(fn)

image-20220809121806237

image-20220809121817987

image-20220809121828969

  • 方法返回值为1:打印函数体,自动调用toString()
  • 方法无返回值(undefined):打印函数体,自动调用toString()
  • 方法返回匿名函数:打印函数体,自动调用toString()之后调用valueOf()

从上面的结果中我们猜测,当打印的内容为函数时,会在内部隐式调用toString(),如果toString()返回基本数据类型,则数据转换到此为止。如果返回对象数据类型,则在toString()之后还会调用valueOf()。

打印语句3和4:

image-20220809121901299image-20220809121914502image-20220809121925724

  • 方法返回值为1:自动调用valueOf(),打印valueOf()返回值
  • 方法无返回值(undefined):自动调用valueOf(),打印valueOf()返回值
  • 方法返回匿名函数:自动调用valueOf()之后调用toString(),报错

有报错最好~从报的错误开始入手

TypeError: Cannot convert object to primitive value

翻译:不能将对象转换为原始值
查阅资料发现引用类型向原始类型转换遵循ToPrimitive规则

⭐️ToPrimitive规则

是引用类型向原始类型转变的规则,它遵循先valueOf后toString的模式期望得到一个原始类型。如果还是没法得到一个原始类型,就会抛出 TypeError。

三、总结

1.明确转换类型:

如alert,console.log明确引用类型是要转换为字符串类型,那么采用的策略是先toString后valueOf。

2.不明确转换类型:

如隐式类型转换,那么采用ToPrimitive规则,遵循先valueOf后toString的模式期望得到一个原始类型。

MDN中valueOf()方法的返回值:

image-20220809110411560

toString方法返回值

20190719180630686

3.返回值:

在复写这两个函数时,函数返回值起着关键作用,可以决定调用几个方法和最后函数的返回值。好好利用这一特点与隐式自动调用的模式,助力成为装逼达人!

4.注意点:

alert会将两个方法最后得到的原始类型值进行返回

image-20220809113133100
但是console.log不会,还是返回函数体。

其他引用类型类似,可以自己动手试试。

四、问题

对于console.log这个操作不太理解,使用alert阻塞。可以看到先提示调用方法,但是确认完alert弹窗之后,居然console.log打印语句出现在了上面。实在是不理解,希望有懂的大佬不吝赐教。

image-20220809153623938 image-20220809153646505

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
Array.prototype.toString = function () {
console.log('array-----调用toString方法')
alert('array-----调用toString方法')
return 1
}
Array.prototype.valueOf = function () {
console.log('array-----调用valueOf方法')
alert('array-----调用valueOf方法')
return 1
}
Function.prototype.toString = function () {
console.log('function-----调用toString方法')
alert('function-----调用toString方法')
return 1
}
Function.prototype.valueOf = function () {
console.log('function-----调用valueOf方法')
alert('function-----调用valueOf方法')
return 1
}
let arr = new Array()
let fn = function () {
console.log('函数内打印')
}
console.log(arr);
console.log(fn);

五、踩坑心得

事情是这样的,学习的时候看到一篇文章,对照着文章中的进行实践操作却得不到一样的结果,于是顺藤摸瓜摸出了这么多瓜。。。从柯里化到隐式调用到隐式类型转换。

image-20220809094045150

六、参考:

记录一下今天安装nvm过程遇到的一些问题~

nvm是一个node的版本管理工具,很适合我这种需要很多node版本的人

我的安装步骤:

  1. 卸载原有的nodejs,我这一步卸得太干净了。。。把之前下的包都删掉了,悲伤悲伤(;´д`)ゞ 其实可以不用卸载原有的nodejs的,nvm可以将现有的nodejs添加到管理中的
  2. nvm下载地址
  3. 安装完成后使用nvm -v检查一下nvm是否安装成功
  4. 安装成功后就可以使用以下命令使用啦:
    • nvm on:开启nvm的nodejs版本管理
    • nvm intall :安装指定版本
    • nvm use :使用指定版本
    • nvm list [available]:显示已安装的列表。可选参数available,显示可安装的所有版本。list可简化为ls。
    • nvm uninstall :卸载指定版本
    • nvm off:关闭nvm的nodejs版本管理

遇到的问题一:无法创建文件夹

image-20231211155947870

解决办法:使用管理员身份打开cmd即可,给程序创建文件夹的权限

遇到的问题二:

1
2
3
4
5
6
7
8
9
10
11
12
PS C:\Users\asus\Desktop\visual_sense_3d>npm i cnpm -g
npm ERR! code ETIMEDOUT
npm ERR! errno ETIMEDOUT
npm ERR! network request to https://registry.npmjs.org/graceful-readlink failed, reason: connect ETIMEDOUT 104.16.25.35:443
npm ERR! network This is a problem related to network connectivity.
npm ERR! network In most cases you are behind a proxy or have bad network settings.
npm ERR! network
npm ERR! network If you are behind a proxy, please make sure that the
npm ERR! network 'proxy' config is set properly. See: 'npm help config'

npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\Deshun\AppData\Roaming\npm-cache\_logs\2023-12-01T03_09_36_539Z-debug.log

解决方法:

1
2
3
4
5
6
1.查看npm镜像设置
npm config get registry
2.将npm设置为淘宝镜像
npm config set registry https://registry.npm.taobao.org
3.再次查看npm镜像设置
npm config get registry

应该是由于我前面安装nvm的时候在setting.txt文件中已经修改了node的镜像为taobao,所以在这个地方也要对应修改

安装完毕,美美用起nvm,真的很方便,随意丝滑切换node版本( ̄︶ ̄*))

Application > Manifest

清单窗格。

:sunflower:作用:

当你希望用户能够将该引用程序添加到Mac OS上的“应用程序”文件夹、Windows上的“开始”菜单以及Android和IOS上的主屏幕时,需要一个Web应用程序清单。设置应用程序清单后,可以打开devtools中的Manifest中检验配置是否正确。

:sunflower:编写Web应用程序清单:

清单文件可采用任何名称,但通常命名为 manifest.json,并从根目录(网站的顶级目录)提供。该规范建议该扩展项应为 .webmanifest,但浏览器也支持 .json 扩展项。

创建清单后,将 <link> 标记添加到渐进式 Web 应用的所有页面。例如:

1
<link rel="manifest" href="/manifest.json">

具体的Manifest编写规则和配置项如下:https://web.dev/articles/add-manifest?hl=zh-cn

Application > Service Worker

Service Worker 窗格。

在该位置我们对Service Worker进行检查和调试

:sunflower:Service Worker :

本质上充当位于 Web 应用程序、浏览器和网络(如果可用)之间的代理服务器。除其他外,它们的目的是创建有效的离线体验、拦截网络请求并根据网络是否可用采取适当的操作,以及更新服务器上的资产。它们还允许访问推送通知和后台同步 API。

:sunflower:特性:

  1. 没有DOM访问权限
  2. 无法动态导入JS模块,如果在Service Worker全局范围内调用import().则会抛出异常,但允许使用import语句进行静态导入
  3. Service Worker被设计为完全异步,在与主JavaScript不同的线程上运行,因此它是非阻塞的,所以同步XHR和Web Storage等API无法在Service Worker内部进行使用
  4. 仅允许通过HTTPS运行

具体编写Service Worker参考如下:https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

Application > LocalStorage

localStorage 用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。

:hatched_chick:localStorage 的优势

  • 1、localStorage 拓展了 cookie 的 4K 限制。
  • 2、localStorage 会可以将第一次请求的数据直接存储到本地,这个相当于一个 5M 大小的针对于前端页面的数据库,相比于 cookie 可以节约带宽,但是这个却是只有在高版本的浏览器中才支持的。

:hatched_chick:localStorage 的局限

  • 1、浏览器的大小不统一,并且在 IE8 以上的 IE 版本才支持 localStorage 这个属性。
  • 2、目前所有的浏览器中都会把localStorage的值类型限定为string类型,这个在对我们日常比较常见的JSON对象类型需要一些转换。
  • 3、localStorage在浏览器的隐私模式下面是不可读取的。
  • 4、localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡。
  • 5、localStorage不能被爬虫抓取到。

localStorage 与 sessionStorage 的唯一一点区别就是 localStorage 属于永久性存储,而 sessionStorage 属于当会话结束的时候,sessionStorage 中的键值对会被清空。

:japanese_ogre:不可以设置过期时间不可以设置过期时间,但是开发者可以自己判断时间然后把它移除,但是它本身是没有设置过期时间的这个操作的

:hatched_chick:人工设置过期时间

其实都是set方法多传个过期时间啦

  1. ES5扩展Storage

    思路很简单,存储的值加一个时间戳,下次取值时验证时间戳。
    注意: localStorage只能存储字符,存入时将对象转为json字符串,读取时也要解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Storage.prototype.setExpire = (key, value, expire) => {
    let obj = {
    data: value,
    time: Date.now(),
    expire: expire
    };
    //localStorage 设置的值不能为对象,转为json字符串
    localStorage.setItem(key, JSON.stringify(obj));
    }

    Storage.prototype.getExpire = key => {
    let val = localStorage.getItem(key);
    if (!val) {
    return val;
    }
    val = JSON.parse(val);
    if (Date.now() - val.time > val.expire) {
    localStorage.removeItem(key);
    return null;
    }
    return val.data;
    }
  2. ES6拓展Storage

    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
    class Storage {

    constructor(props) {
    this.props = props || {}
    this.source = this.props.source || window.localStorage
    this.initRun();
    }
    initRun(){
    /*
    * set 存储方法
    * @ param {String} key 键
    * @ param {String} value 值,存储的值可能是数组/对象,不能直接存储,需要转换 JSON.stringify
    * @ param {String} expired 过期时间,以分钟为单位
    */
    const reg = new RegExp("__expires__");
    let data = this.source;
    let list = Object.keys(data);
    if(list.length > 0){
    list.map((key,v)=>{
    if( !reg.test(key )){
    let now = Date.now();
    let expires = data[`${key}__expires__`]||Date.now+1;
    if (now >= expires ) {
    this.remove(key);
    };
    };
    return key;
    });
    };
    }

    set(key, value, expired) {
    /*
    * set 存储方法
    * @ param {String} key 键
    * @ param {String} value 值,
    * @ param {String} expired 过期时间,以毫秒为单位,非必须
    */
    let source = this.source;
    source[key] = JSON.stringify(value);
    if (expired){
    source[`${key}__expires__`] = Date.now() + expired
    };
    return value;
    }

    get(key) {
    /*
    * get 获取方法
    * @ param {String} key 键
    * @ param {String} expired 存储时为非必须字段,所以有可能取不到,默认为 Date.now+1
    */
    const source = this.source,
    expired = source[`${key}__expires__`]||Date.now+1;
    const now = Date.now();

    if ( now >= expired ) {
    this.remove(key);
    return;
    }
    const value = source[key] ? JSON.parse(source[key]) : source[key];
    return value;
    }

    remove(key) {
    const data = this.source,
    value = data[key];
    delete data[key];
    delete data[`${key}__expires__`];
    return value;
    }

    }
:hatched_chick:存取格式

localStorage 只支持 string 类型的存储,存入数字取出来的类型也是字符串

需要存入对象时先使用Json.stringify转换为json字符串,取出之后再转换为对象

1
2
3
4
5
6
7
8
9
10
11
12
var storage=window.localStorage;
var data={
name:'xiecanyong',
sex:'man',
hobby:'program'
};
var d=JSON.stringify(data);
storage.setItem("data",d);
//将JSON字符串转换成为JSON对象输出
var json=storage.getItem("data");
var jsonObj=JSON.parse(json);
console.log(typeof jsonObj);
:hatched_chick:增删改查
1
2
3
4
localStorage.setItem("key", "value");
var lastname = localStorage.getItem("key");
localStorage.removeItem("key");//删除某个键值对
localStorage.clear()//删除所有localStorage

Application > SessionStorage

sessionStorage 用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据。

:ant:存取格式

和localStorage相同

:ant:增删改查
1
2
3
4
sessionStorage.setItem("key", "value");
var lastname = sessionStorage.getItem("key");
sessionStorage.removeItem("key");
sessionStorage.clear();

Application > IndexDB&WebSql

这两者主要用在前端有大容量存储的页面上,例如,在线编辑浏览器或者网页邮箱

介绍 容量 状态
WebSql 关系型数据库 大小限制因浏览器而异,但一般为5MB-50MB的范围 被W3C标准废弃
IndexDB 非关系型数据库 存储大小是250m以上(受计算机硬件和浏览器厂商的限制) 正常使用


  • 原生的js滚动在移动端上会发生卡顿,没有惯性和首尾弹簧功能,于是选择Better Scroll这个框架来适配移动端的滚动
  • 优点:使用原生JS,不依赖任何框架,体积小

BScroll的安装

安装:npm install better-scroll --save

引用:

  1. vue项目:import BScroll from 'better-scroll'
  2. js项目:直接下载bscroll.js文件然后引入就可以

使用

HTML部分:

  • 在最外面需要包一层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="content">
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>

JS部分:

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
//实现滚动功能
const bscroll = new BScroll(document.querySelector('.content'),{});


//监听实时滚动位置
const bscroll = new BScroll(document.querySelector('.content'),{
probeType:2
//0或者1都是不检测实时位置
//2:在手指滚动的过程中侦测,手指离开后的惯性滚动过程不侦测
//3:在手指滚动和惯性滚动过程都进行侦测
});


//上拉加载更多
const bscroll = new BScroll(document.querySelector('.content'),{
pullUpLoad: true
});
bscroll.on('pullingUp',() => {
console.log('发生上拉加载动作,发送网络请求的代码可以写在这里');

//等数据请求完成,并且将新的数据展示出来之后
//调用这个函数之后才能进行下一次的下拉加载更多
setTimeout(() => {
bscroll.finishPullUp()
},2000)
})

点击回到顶部

对new出来的BScroll对象使用scrollTo方法

bscroll.scrollTo(0,0,500)500毫秒回到顶部

缺点

当滚动内容里面有图片时会偶尔出现bug,可能在 BS计算滚动内容的高度的时候图片还没加载过来,这时会导致记得算出来的高度比实际的高度低出现滚动问题

解决办法:图片的@load监听图片加载完成再对BS进行refresh

vue中css文件引入

import "./normalize.css"

CSS中的变量

  1. :root获取根元素html

  2. 定义变量

    1
    2
    3
    :root{
    --color-text: #666
    }
  3. 使用变量

    1
    2
    3
    div{
    color: var(--color-text)
    }

用在设置多个相同颜色的时候很方便:D

position: sticky

使用该属性的时候要设置top属性

当该元素小于这个高度时正常滚动,当该元素大于这个高度时会被固定在这个高度

好用的属性一般不兼容 :(

神奇属性在哪里

1
2
3
4
5
6
7
8
9
div{
display: flex;
flex-wrap: wrap; /*去掉会让所有的子元素排列在一排不会换行噢*/
justify-content: space-evenly;
}

div>div{
width:47%;
}

可以做到div里面的div元素flex布局,而且一行刚好两个(通过调整子元素的宽度可以决定一行里面有多少个子元素),再通过justify-content属性可以让它们之间的间隙各种等分!间隙等分,子元素等分都有,建议实践出真知,真的超级神奇

为了实现实时更新或类似于聊天室的应用

  1. 一开始想到的肯定是配合setTimeout每隔一小段时间就给服务器发送请求,但是这样给服务器的压力很大,不够随机应变太耗费资源

  2. 第二个尝试地是长轮询,顾名思义,是服务器发出一个时间比较长的请求。比如时间设定为30秒,在30秒之内,如果收到服务器更新的数据,则进行对应的操作之后再发出请求,如果在30秒之内没有收到返回数据则执行error回调函数继续发出请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function Ask(){
    $.ajax({
    url: "http://47.92.139.26:8080/spring-boot-panda-0.0.1-SNAPSHOT/question/getNew/" + lastId + "/" + questionId
    dataType: "json",
    async: "true",
    timeout: "30000",
    //服务器发出一个30秒的请求
    success: function(msg) {
    done
    Ask();
    },
    error: function() {
    done
    Ask();
    }
    })
    }

    这种操作比起第一种肯定会比较节约资源,但是本质还是没有改变,还是客户端和服务器之间的单向对话,不够随机应变:D

  3. 最后使用了WebSocket,操作简单体验感很好:accept:

    (记得引入对应的WebSocket包

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
//打开双通道
function connect(){
var socket = new SockJS('http://47.92.139.26:8080/spring-boot-panda-0.0.1-SNAPSHOT/endpointAnswer');
//新建一个WebSocket对象

stompClient = Stomp.over(socket);
//使用STMOP子协议的WebSocket客户端

stompClient.connect({},function(frame){
//连接WebSocket服务端

//广播接收信息
stompTopic();
QuesTest = 1;
localStorage.setItem('QuesTest', QuesTest);

});
}

//关闭双通道
function disconnect(){
if(stompClient != null) {
stompClient.disconnect();
}
console.log("Disconnected");
}

//广播(一对多) = 接收服务器传过来的消息
function stompTopic(){
//通过stompClient.subscribe订阅目标(destination)发送的消息(广播接收信息)
stompClient.subscribe('/answerMass/getNew',function(response){
done
});
}

//群发 = 发送消息
function sendMassMessage(){
done
}

WebSocket和之前两种方式的原理都不一样,WebSocket可以打开双通道,做到请求不止可以有客户端发起也可以有服务器发出来,实现了双向对话,节约资源。一般有四个函数:打开双通道,接收服务器发送的消息,给服务器发消息,关闭双通道。

总结: WebSocket真香!

0%