likes
comments
collection
share

ts(js)中那些似懂非懂的知识,我猜你肯定中招过

作者站长头像
站长
· 阅读数 15

先填上篇文章提到的函数双向协变的坑

函数到底是不是双向协变的?

如果你回答说开启 strictFunctionTypes 不是,不开启就是。请看下题

// 假设开启了 strictFunctionTypes
interface Person {
  name: string;
  age: number;
}


// Student 是 Person 的子类型
interface Student extends Person {
  getStatus(): string;
}

interface Worker extends Person {
  coding: boolean;
}

// 注意 method 这个方法的定义方式
interface Test<T> {
  method(a: T): number;
}

declare let studentTest: Test<Student>;
declare let personTest: Test<Person>;

// 不会报错,strictFunctionTypes 没有生效
personTest = studentTest; // Ok 
studentTest = personTest; // Ok

// ⚠️ 如果我们改下 Test 中 method 的定义方式

// 注意 method 这个方法的定义方式
interface Test<T> {
  method: (a: T) => number;
}

// 这次达到了预期的效果
personTest = studentTest; // error
studentTest = personTest; // ok

强调总结下:函数作为对象上的属性的时候,他定义方式不同。效果完全不一样

函数方法

不受 strictFunctionTypes 设置的影响,始终保持双向协变。

interface Test<T> {
  // 这种叫函数方法
  method(a: T): number;
}

函数属性

interface Test<T> {
  // 这种叫函数方法
  method(a: T): number;
}

受 strictFunctionTypes 设置的影响,为true则是逆变,否则双向协变。

为什么

其实只要把ts编译成js,我们就明白了。

class Test {
  method() {
    // 这是函数方法
  }
  property = () => {
    // 这是函数属性
  }
}

// 编译后
var Test = /** @class */ (function () {
    function Test() {
        var _this = this;
        this.property = function () {
            // 这是函数属性
        };
    }

    Test.prototype.method = function () {
        // 这是函数方法
    };

    return Test;
}());

method 被写入了原型对象上,构造函数的通用方法也写在了原型上的。

还记得之前文章中,我们提到为了兼容 Array 这样的数据结构,才导致函数最开始被设计成双向协变吗?

这就是为什么函数方法只能是 双向协变。所以强烈推荐使用 函数属性

双向协变无法保证类型安全,所以在使用 双向协变 的数据结构一定要小心!

官方解释

// 不安全的示例
const students: Student[] = [];

const arr: Person[] = students;

arr1.push({} as Worker);

// 这里是遍历 students 变量,别看错了
// 就是因为 students 赋值给 arr, 而 arr 调用的 students 上的push 导致的不安全行为
students.forEach((item) => item.getStatus);

class (类)的类型该如何表示?

在ts中,class是少有的几个既可以当值使用,又可以当成类型使用的数据结构。

const ImNull: null = null
const ImUndefined: undefined = undefined

class Test {
    show(){}
}
// ???
const test: Test = new Test()

细心的朋友发现了,Test 作为类名, 作为类型来使用的时候,表示的是该类的实例。而不是类本身

意思就是 const test: Test = Test 这种写法是错误的。

正确的写法是 const test: typeof Test = Test

如果我稍微改一下 Test 的定义呢?

class Test {}

// 请问下面是否会报错
const test: Test = Test
// 答案在下文

构造函数

下面这些写法的含义你能准确理解吗?

declare let fn: Function
declare let str: String
declare let obj: Object

或者下面成立吗?

fn = () => {}
fn = (...args: string[]) => {}
fn = false
fn = Boolean
fn = alert
查看答案 ✅ ✅ ❌ ✅ ✅
str = 'i am string'
str = String
str = Function
str = alert
查看答案 ✅ ❌ ❌ ❌
obj = 'i am string'
obj = Object
obj = function() {}
obj = alert
obj = false
obj = null
查看答案 ✅ ✅ ✅ ✅ ✅ ❌

如果你能答对上面的全部问题,相信你的js基础一定不错!

首先,构造函数和类的概念要搞明白。其实上面已经剧透过了,类只是一个构造函数的一个语法糖。所以我们可以把构造函数看作类来处理

那么,declare let fn: Function 的含义就是 fn 的类型是 Function 的实例. 如果你熟悉 js 的原型链,上面的问题就简单了

fn = () => {} // 这是箭头函数,所以 ok
fn = (...args: string[]) => {}  // 这是也是箭头函数,所以 ok
fn = false // 
fn = Boolean // Boolean 作为构造函数,他也是 Function 的实例。我们可以通过 isntanceOf 来判断
fn = alert // 内置函数,ok

如果对原型链不太熟悉或者忘了,可以参考我之前的 文章

String 比较简单,就不解释了。下面看下Object

obj = 'i am string' // 这是正确的,因为 String 继承 自 Object。 'i am string' 是 String 的实例
obj = Object // Object 本身是函数,所有的函数都是 Function 的实例。 Function 又继承 Object。 所以这是正确的
obj = function() {} // 同上
obj = alert // 同上
obj = false // false 是 Boolean 的实例, Boolean 继承 Object。 所以正确
obj = null // 很明显错误

JS的圈子果然太复杂了,Function 和 Object 的私生活太乱了

友情提示,构造函数就是函数,几乎没有什么区别(除了箭头函数)。可以被 new 关键词调用的函数就叫构造函数。

Object、object、{} 和 Record 的恩怨纠缠

说实话,最开始我写TS的时候,也老是感到困扰。就想定一个对象,怎么还有这么多的方式。不过现在基本都是用 Record

Object 上面已经讲过,不再赘述

object 很特殊,他表示非原始类型。即除开 boolean、string、number、null、undefined、symbol 这些原始类型。还记得 WeakMap 这个数据结构吗?

// 它的键就要求:非原始类型
interface WeakMap<K extends object, V> { }

{} 这个则表示一个空对象。 类似 const obj = new Object() 这个意思。他本身没有任何属性,但是可以访问 Object 上的默认方法和属性

不过现在有了 Record 过后,上面都是可以平替的。

object 可以写成 Record<string, unknown>

{} 可以写成 Record<string, never>

最后再提一下另一个非常不好的写法,如果发现你同事这样写,就赶紧艾特他过来,看我锤死他 :)

interface InputProps {
    name: string
    value?: string
    // 这个写法是非常非常不好的,就比 any 强一点
    [x: string]: any
}

完结撒花~