前端学习踩坑(1)

笔者这两天正在进行JavaScript与TypeScript基础项目的编写,涉及到的问题多为基础

关于TS与JS的爱恨情仇

通过属性的key访问value

众所周知,js中我们在定义了某个对象的属性的前提下,可以直接使用key来访问该属性的value,如下:

1
2
3
4
5
class User {
name: "aaa"
}
let user = new User();
console.log(user[name])

在显式的类定义下,ts也可以做到相同的事情,写法仅仅是添加一个属性的类型

1
2
3
4
5
class User{
name : string = '111'
}
let user = new User();
console.log(user.name)

但是在某些情况下,我们需要遍历某个对象里的所有属性,并且可能这个对象的定义对于我们来说是非显式定义的,即我们也不知道他的类型,所以我们在ts中便出现了以下的问题:

1
2
3
4
5
6
function span(user : object) {
Object.keys(user).forEach(key => {
console.log(user[key]);
}
)
}

image-20220717112800844

由于引用的user类型是我们自己显式定义的User,我们可以使用以下方法来解决:

1
2
3
4
5
function span <T extends object> (user : T) {
for (let key in user) {
console.log(user[key]);
}
}

这是使用泛型来解决这种报错的技巧,同时我们也可以使用断言:

1
2
3
4
5
function span2 (user : User) {
for (let key in user) {
console.log(user[key as keyof typeof user]);
}
}

keyof

在ts中,我们可以使用keyof关键字来获取一个对象中所有的key,然后返回一个新的联合类型

1
2
3
4
5
6
7
class User{
// [key : string] : any;
name : string = '111';
power : number = 0;
}
type keys = keyof User;
// "name" | "power"

typeof

通过使用typeof关键字,我们可以获得一个对象的type,这常常被使用在根据对象类型的不同而编写分支语句的情况下。

回到上文

1
2
3
4
5
function span2 (user : User) {
for (let key in user) {
console.log(user[key as keyof typeof user]);
}
}

通过使用typeof user,我们获得了User类型,再通过使用keyof User,我们获得了User中的所有key的联合类型,这样我们就可以正常的遍历对象中的属性了。

所以说,我们也可以这么写

1
2
3
4
5
function span3 (user : User) {
for (let key in user) {
console.log(user[key as keyof User]);
}
}

为对象动态增添属性

这听起来很简单,在js中你只需要object[key] = value,这个对象就优雅的新增了一个属性,但是在ts中,这可让我犯了难。

我尝试使用一个自定义的封装好的{key : any, value : any}对象的数组来存储新增的类型,只需要每次新建一个这个对象的实例,然后push进数组就大功告成了。

但是!有一个很严重的问题,对象中属性的存储是一种字典的键值对的存储模式,而不是数组,即对于相同的key,永远只能存在一个value,但是数组的话便会一直新增实例的数量。(这也是我找了一早上的bug,为什么越用越卡)

QQ图片20220717175253

惨案现场,浏览器真是最好的调试器!

所以我们有一个更加优雅的方式,新建一个类用于封装 + 模拟字典

1
2
3
4
5
6
7
8
9
10
11
12
13
class Methods {
[key : string] : any;
}
class Vue {
...
methods : Methods;
...
constructor(options : any) {
this.methods = new Methods();
...
}
...
this.methods[key] = value;

可以看到,我们通过使用数组来当做key,这使得我们在添加时,能够很容易的直接添加新的属性,同时也能直接修改key对应的value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Methods {
[key : string] : any;
}
test('demo', () => {
let methods = new Methods();
methods['1'] = '1';
console.log(methods);
methods['2'] = 2;
console.log(methods);
methods['1'] = 111;
console.log(methods);

})
// 控制台输出
// Methods { '1': '1' }
// Methods { '1': '1', '2': 2 }
// Methods { '1': 111, '2': 2 }

React组件

函数式组件

我们可以通过以下两种方式创建函数式组件:

1
2
3
4
5
6
7
8
9
// 1.创建函数式组件 
function Demo(){
console.log(this); //这个this是undefined,因为babel编译之后开启了严格模式
return <h2>w shi yi ge zu jian</h2>
}
//函数式组件还可以添加参数
function Show(props){
return <h1>Hello {props.age}</h1>
}

然后将组件渲染出来

1
2
ReactDOM.render(<Demo />,document.getElementById("test"));
ReactDOM.render(<Show age="ss" />,document.getElementById("div"));
  • 组件的首字母必须是大写,并且有返回值

  • 在渲染的时候必须<组件名 />

即React先解析组件标签,若组件的标签为小写开头,则自动匹配HTML中已经预设好的标签,只有当标签为大写字母开头,React才会解析组件标签并找到相应的组件。在发现组件是函数定义的后,调用函数,并将返回的虚拟DOM转化为真实DOM,并渲染在页面中。

类式组件

1
2
3
4
5
6
7
8
 // 1.创建类式组件 [必须继承React.Component]
class MyComponent extends React.Component{
render(){
//render是放在原型对象上的,供实例对象使用
//render的this,MyComponent的实例对象【组件实例对象】
return <h1>这个是类组件</h1>
}
}

然后将组件渲染出来:

1
ReactDOM.render(<MyComponent/>, document.getElementById('text'));

渲染过程和上面基本相同。

下面是一个基本的类组件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
isHot : true
}
this.click = this.click.bind(this);
}

render() {
return <h1 onClick={this.click}>今天天气很{this.state.isHot ? '炎热' : '凉爽'}</h1>;
}

click() {
this.setState({isHot : !this.state.isHot});
}

}

添加属性时,可以使用增加父类中的state属性来修改,但是更加容易的方法是直接在子类中继承父类的state属性并直接进行增删改。

添加方法时,需要考虑到一个问题,非静态方法应该由类的实例来调用,若是在虚拟dom中直接触发,会导致click中的this未定义,所以我们要首先将类中的方法的this进行赋值,即使用bind,才能正确的调用函数。

为了简化,我们可以使用箭头函数会搜寻外侧的实例作为this的特性,对函数中的this进行正确的赋值,简化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Weather extends React.Component {
state = {
isHot : true
}

render() {
return <h1 onClick={this.click}>今天天气很{this.state.isHot ? '炎热' : '凉爽'}</h1>;
}

click = () => {
this.setState({isHot : !this.state.isHot});
}

}

Jest单元测试

通过使用Jest + jsdom,可以模拟dom进行我们自编写vue组件的测试。

在终端使用

1
jest --init

在其中的测试环境中选择jsdom,并安装对应依赖

1
2
3
"dependencies": {
"jest-environment-jsdom": "28.1.2",
}
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
const {Vue} = require("../src/index.js");

function renderVueComponent() {
let vm = new Vue({
el: 'app',
data: {
msg: 'hello world',
info: ''
},
methods: {
handleClick: function () {
// @ts-ignore
console.log("handleClick", this.msg);
}
}
})
}

test('v-text-test', () => {
document.body.innerHTML = `
<div id="app">
<p v-text="msg"></p>
</div>
`;
renderVueComponent();
expect(document.body.innerHTML).toMatch(/hello world/);
});

引入测试类后,通过对document.body.innerHTML的修改,创建不同的dom用于测试。在这个测试方法中,我们测试了v-text组件的实现。在正常运行的情况下,<p>标签中应该有msg的元素,即’hello world’,使用toMatch即可判断结果。