javascript基础知识强化

343 阅读10分钟
  • 类型转换中 Number(undefined) 结果为 NaNNumber(null) 结果为 0

  • **作为求幂运算符也可作为开方使用,2 ** 3 等于 8,8 ** (1/3) 等于 2

  • 大多运算符执行后都会返回当前值如+5-3,赋值运算符=也一样,如console.log(x = 5),x 被赋值为5,并打印出返回的 x 的值,在一些判断中常看到,如:

    where(x = y - 1){
       ... 
    }
    

    还有链式赋值中也经常用到,如:

    let a, b, c;
    a = b = c = 2 + 2;
    

    但不建议在代码中这么写,可读性差

  • 逗号运算符,是比赋值运算符优先级还要低的一种运算符,const a = (1 + 2, 3 + 4);a 的结果为 7 ,1 + 2 和 3 + 4 都会执行,但是只有最后的语句的结果会被返回,1 + 2 运行了但结果被丢弃了。这样的运算符在把几个行为放在一行上来进行复杂的运算时非常有用,如:

    for (a = 1, b = 3, c = a * b; a < 10; a++) {
        ... 
    }
    
  • 空值合并运算符 ?? 和逻辑运算符 || 类似,但是 ?? 只判断第一个值是否为 nullundefined,如果是第一值个是 undefinednull,结果才为第二个值

    const a = null;
    const b = undefined;
    const c = 0;
    const d = '';
    const v = 'value';
    console.log(a ?? v , a || v); // 结果为 'value' 'value'
    console.log(c ?? v , c || v); // 结果为 0 'value'
    console.log(d ?? v , d || v); // 结果为 '' 'value'
    
  • 对象有 .[] 两种查询和更改属性的方式,如 obj.keyobj[key],当需要用到复杂属性(变量、表达式、多词属性等)时大多用 [] 如:

    const key1 = 'key1';
    const key2 = 'key2';
    const key3 = 'key3 key3';
    const obj = {
        key1: 'value1',
        key2: 'value2',
        'key3 key3': 'value3',
        [key1+key2]: 'value12',
    };
    console.log(obj.key1,obj.key2); // 'value1' 'value2'
    console.log(obj[key1+key2],obj[key3]); // 'value12' 'value3'
    
  • 可选链 ?. 是一种访问嵌套对象属性的安全的方式。如获取一个user对象的 address 中的 street,必须对 user 和 address 做是否存在的判断,否则当 user 或 address 为 null / undefined 时都将触发TypeError。可选链 ?. 如果前面的部分是 undefined 或者 null,它会停止运算并返回

    const getUserStreet1 = user => {
        if(user && user.address) {
            return user.address.street;
        }
    }
    
    const getUserStreet2 = user => {
        return user?.address?.street;
    }
    

    也可以对函数、数组使用可选链 ?.()?.[]

    const print1 = undefined;
    const print2 = x => x;
    console.log(print1?.('test')); // undefined
    console.log(print2?.('test')); // test
    
    const getArrValue = (arr, index) => arr?.[index];
    console.log(getArrValue(undefined, 1)); // undefined
    console.log(getArrValue([1,2], 1)); // 2
    
  • Symbol([desc]) 函数可以创建一个唯一的 symbol类型的值,相同的描述文字也将创建出不同的值

    const symbol1 = Symbol('test');
    const symbol2 = Symbol('test');
    console.log(symbol1 === symbol2); // false
    

    可以用于对象的私有属性,不对外部开放。如果希望同名描述下的 Symbol值相等可以使用Symbol全局注册表 Symbol.for(key) 如果同样的key将返回之前这个值,否则将创建一个新的值返回

    const symbol1 = Symbol.for('test');
    const symbol2 = Symbol.for('test');
    console.log(symbol1 === symbol2); // true
    

    js 中有许多系统 Symbol,如Symbol.iteratorSymbol.toPrimitive等,可以使用Symbol.toPrimitive(须返回一个原始值) 来设置对象原始值的转换,优先级高于toString()valueOf()

    const obj = {};
    // hint = "string"、"number" 和 "default" 中的一个
    obj[Symbol.toPrimitive] = function(hint) { 
        // 必须返回一个原始值(string、number、boolean等)
    }
    

    hint 参数为 "string"、"number" 和 "default" 中的一个,当进行数学计算、比较或显示进行Number()转换时,hint为 "number";进行alert()打印,对象属性赋值等明显字符串操作时hint为 "string";当运算符“不确定”期望值的类型时hint为 "default",如二元+可以用于数字,也可以用于字符就不能确定结果

    const obj = {};
    obj[Symbol.toPrimitive] = function(hint) {
       return hint === "number" ? 8 : hint; 
    }
    console.log(+obj, Number(obj)); // 8 8
    const test = {
        [obj]: 'test',
    }
    console.log(String(obj), Object.keys(test)); // 'string' ['string']
    console.log(obj + 5); // 'default5'
    

    如果要满足 a == 1 && a == 2 && a == 3+a === 1 && +a === 2 && +a === 3可以用到类型转换

    (() => {
        const a = { v: 0 };
        a[Symbol.toPrimitive] = function(hint) {
            this.v += 1;
            return hint === "number" ? this.v : hint;
        }
        console.log(+a === 1 && +a === 2 && +a === 3); // true
        console.log(+a === 1); // false,因为对象 a 中的 v 值每次在递增
    })();
    

    求解sum(1)(2)(3)(4)...结果为1+2+3+4+...

    const sum = arg => {
        const result = arg2 => sum(arg+arg2);
        result[Symbol.toPrimitive] = () => arg;
        return result;
    } // 使用了闭包、递归、Symbol.toPrimitive
    console.log(+sum(1)(2)(3)(4)); // 10
    console.log(+sum(1)(2)(3)(4)(5) === 15); // true
    

    Symbol.iterator 要求这个方法必须返回一个迭代器(一个有 next() 方法的对象),next() 方法返回的结果的格式必须是 {done: Boolean, value: any},当 done=true 时,表示迭代结束,否则 value 是下一个值。

    const obj = {};
    obj[Symbol.iterator] = function() { 
        // 必须返回一个返回一个迭代器
        return {
            next:() => {
                ...
                // 返回的数据格式必须是 `{done: Boolean, value: any}`
            }
        }
    }
    

    比如迭代 const range = { from: 1, to: 5 };

    const range = { from: 1, to: 5 };
    range[Symbol.iterator] = function() {
        let indexItem = this.from;
        return {
            next: () => {
                if(indexItem <= this.to) {
                    return {done: false, value: indexItem++}
                }else{
                    return {done: true}
                }
            }
        }
    }
    for(let item of range) {
        console.log(item); // 1 2 3 4 5
    }
    

    或者用 generator 改写如下:

    const range = { from: 1, to: 5 };
    range[Symbol.iterator] = function* () {
        let indexItem = this.from;
        for(let indexItem = this.from; indexItem <= this.to; indexItem++){
            yield indexItem;
        }
    }
    for(let item of range) {
        console.log(item); // 1 2 3 4 5
    }
    

    可以使用Object.prototype.toString或者instanceof获取类型,Symbol.toStringTag属性可以自定义对象toString方法获取得类型

    console.log(Object.prototype.toString.call([])); // [object Array]
    const user = {
        [Symbol.toStringTag]: "User",
    }
    console.log(Object.prototype.toString.call(user)); // [object User]
    console.log(window[Symbol.toStringTag]); // Window
    
  • 调用数字的方法时除了可以使用包装器(如:Number(3).toFixed(1))外还可以使用 .. 直接调用,如 3..toFixed(1)3..toString(2)3.toFixed(1)会报错,因为js 语法隐含了第一个点之后的部分为小数部分。如果再放一个点,那么 js 就知道小数部分为空,所以当前面的数字本身就是小数时就不能使用..,如3.5..toFixed(2)会抛出错误,已经有小数点时可直接调用数字的方法3.5.toFixed(2),也可以使用括号(3.5).toFixed(2)、(3).toFixed(2)

  • Array.from()可以将一个可迭代对象(包含Symbol.iterator属性)或一个类数组对象(有索引和 length 属性)转换为正在的数组

    const arrayLike = { 0: "Hello", 1: "World", length: 2 };
    const arr1 = Array.from(arrayLike);
    console.log(arr1); // ['Hello', 'World']
    
    const iteratorObj = { from: 1, to: 5 };
    iteratorObj[Symbol.iterator] = function() {
        let indexItem = this.from;
        return {
            next: () => {
                if(indexItem <= this.to) {
                    return {done: false, value: indexItem++}
                }else{
                    return {done: true}
                }
            }
        }
    }
    const arr2 = Array.from(iteratorObj);
    console.log(arr2); // [1, 2, 3, 4, 5]
    
  • map是类似对象的可迭代数据结构,它的key可以是任意值,获取和修改值通过get()、set()方法,通过对象的entries数据格式可以和对象相互转换

    const obj = {
        key1: 'a',
        key2: 'b',
    };
    const objEntries = Object.entries(obj);
    const map = new Map(objEntries);
    map.set('key3','c');
    const mapEntries = map.entries();
    const resObj = Object.fromEntries(mapEntries);
    console.log(resObj); // {key1: 'a', key2: 'b', key3: 'c'}
    
  • set是一堆 “值的集合”(没有键),也是可迭代对象,它的每一个值只能出现一次。通过add()、delete()方法来操作集合,通过has()方法来判断值是否存在

    const set = new Set();
    const user1 = {name: 'zhangsan', age: 18};
    const user2 = {name: 'lisi', age: 28};
    const user3 = {name: 'wangwu', age: 38};
    set.add(user1);
    set.add(user1);
    set.add(user2);
    set.add(user2);
    set.add(user3);
    set.add(user3);
    console.log(set.size); // 3
    console.log(set.has(user1)); // true
    console.log(set.has({name: 'zhangsan', age: 18})); // false
    
  • 使用setTimeout的嵌套可以实现setInterval,更可控每次执行的频率,比如跟随系统访问量降低轮询请求的频率等

    (() => {
        let mockVisitNum = 0;
        const request = () => console.log('mock request');
        let delay = 1000;
        const loopReq = () => {
            setTimeout(function fun() {
                request();
                const nextDelay = delay + 500 * Math.trunc(mockVisitNum/10);
                console.log(`mockVisitNum:${mockVisitNum},delay:${nextDelay}`);
                setTimeout(fun, nextDelay);
            }, delay);
        }
        loopReq();
        setInterval(() => { mockVisitNum += 1 }, 500);
    })()
    
  • bindapplycall都可以改变函数中this的指向

    • bind(context, arg1, arg2…): 调用后改变了函数内部this的指向为context,并生成新的函数,新的函数并不会有原来函数的属性
    • call(context, arg1, arg2…):调用后改变了函数内部this的指向为context,并立即执行函数
    • apply(context, args):和call的作用一样,区别在于传参的不同,apply接受的第二个参数是类数组参数,而call后面的参数是以逗号隔开的 例:实现lodash里面类似partial(func, [partials])的功能,这个方法类似bind,但它不会绑定 this,如下:
    const greet = function(greeting, name) { 
        return greeting + ' ' + name;
    }; 
    const sayHelloTo = _.partial(greet, 'hello');
    sayHelloTo('fred'); // => 'hello fred'
    

    使用箭头函数和bind的可以很容易实现const partial = (fn, ...args) => fn.bind(null,...args);,需要注意箭头函数没有this的问题,如下:

    const user1 = {
        name: 'userTest',
        print: function() {
            console.log(this.name);
        }
    }
    const user2 = {
        name: 'userTest',
        print: () => {
            console.log(this.name);
        }
    }
    user1.print(); // 'userTest'
    user2.print(); // undefined,箭头函数中的this被指向执行时的全局上下文了(globalThis)
    

    但被操作函数是对象里的函数时,const partial = (fn, ...args) => fn.bind(null,...args);这种方法会导致内部的this丢失

    const partial = (fn, ...args) => fn.bind(null,...args);
    const user = {
        name: 'userTest',
        print: function(job, age) {
            console.log(job, this.name,age);
        }
    }
    user.print = partial(user.print, 'teacher');
    user.print('18'); // teacher undefined 18,this.name获取失败
    

    所以实现的考虑函数可能调用this的情况,而这里不能直接将null改为this,此时的this是全局对象,而一但绑定函数fn内部的this将全部被持久化到全局对象,而不是执行时的上下文。可以返回一个具有运行时上下文的函数(非箭头函数),在调用时分配内部this到执行的上下文就能保证内部this的正确性

    const partial = (fn, ...args) => {
        // 本身返回了一个function,所以内部的返回值需要立即执行fn
        return function(...fnArgs) {
            // 此出的this根据调用时决定
            return fn.call(this, ...args, ...fnArgs);
        }
    }
    
    const greet = function(greeting, name) { 
        return greeting + ' ' + name;
    }; 
    const sayHelloTo = partial(greet, 'hello');
    sayHelloTo('fred'); // 'hello fred'
    
    function print(job, age) {
        console.log(job, this.name, age);
    }
    const teachers = [{name: 't1', print: partial(print, 'teacher')}];
    const students = [{name: 's1', print: partial(print, 'student')}];
    teachers[0].print('38'); // teacher t1 38
    students[0].print('18'); // student s1 18
    
  • 对象有两种属性,普通的数据属性和访问器属性(getter/setter),访问器属性的本质上是用于获取和设置值的函数(可以对要设置或获取的属性进行拦截、过滤、加工等操作),但从外部代码来看就像常规属性

    const user = { 
        name: "John",
        surname: "Smith",
        get fullName() {
            return `${this.name} ${this.surname}`; 
        },
        set fullName(value) {
            [this.name, this.surname] = value.split(' ');
        },
    };
    console.log(user.fullName); // John Smith
    user.fullName = 'test fullName';
    console.log(user.fullName); // test fullName
    
  • 对象中操作原型的方法有

    • Object.getPrototypeOf(obj) 返回对象 obj 的 [[Prototype]]
    • Object.setPrototypeOf(obj, proto) 将对象 obj 的 [[Prototype]] 设置为 proto
    • Object.create(proto, [descriptors]) 利用给定的 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象。 应该使用上述方法来替代 obj.__proto____proto__本身只是作为[[Prototype]]的访问器属性,它会 set/get [[Prototype]]。正是这个原因,如果有一个对象将要实现接受所有的属性时会发现__proto__作为字符串时并不能正确的赋值到对象中,因为__proto__被作为对象的原型对象访问器属性只允许为null或一个对象
    const obj = {};
    obj.__proto__ = 1234;
    console.log(obj.__proto__); // 一个原型对象
    console.log(obj.toString); // [object Object]
    

    如果要解决这个问题,可以使用Object.create(null) 创建了一个空对象,这个对象没有原型([[Prototype]] 是 null),这个对象非常的干净比 {} 还简单,同样也有他的缺点,他就没有了对象本身的一些像toString之类的方法,此时__proto__紧紧作为一个对象的属性而不能再操作对象的原型了,需要用getPrototypeOf(obj)、 setPrototypeOf(obj, proto)才能操作。

    const obj = Object.create(null);
    obj.__proto__ = 1234;
    console.log(obj.__proto__); // 1234
    console.log(obj.toString); // undefined
    
  • class关键字是构造函数的变体,但不完全是语法糖,会有一些额外的标识等区别

    class User {
        constructor(name){
            this.name = name;
        }
    
        getName(){
            return this.name;
        }
    }
    const user = new User('zhangsan');
    console.log(user.getName()); // zhangsan
    

    这段用class编写的代码相当于如下代码

    function User(name) {
        this.name = name;
    }
    User.prototype = {
        constructor:User,
        getName() {
            return this.name;
        }
    }
    

    但是在他们的[[Prototype]]可以看到一些差别

    image.png ____________________ image.png

    类方法不可枚举。 类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false

    image.png

    image.png

  • js中有 常见的几种继承方式,对于class类也有继承,通过extends关键字实现,原理上差不多,都是通过原型链的方式实现

    class Animal {
        constructor(name) {
            this.name = name;
        }
        
        getName() {
            return this.name;
        }
    }
    
    class Rabbit extends Animal {
        constructor(name, age) {
            super(name); // 必须在赋值前使用super关键字
            this.age = age;
        }
        
        getInfo() {
            return {
                name: super.getName(),
                age: this.age,
            }
        }
    }
    
    const rabbit = new Rabbit('xiaobai', 3);
    console.log(rabbit.getInfo()); // {name: "xiaobai", age: 3}
    

    在子类的 constructor 函数中必须在 this 对象属性赋值(如:this.age = age)前使用super,原因如下

    class Animal {
        name = 'animal'; 
        constructor() { 
            console.log(this.name);
        } 
    }
    class Rabbit extends Animal {
        name = 'rabbit';
    }
    const animal = new Animal(); // animal 
    const rabbit = new Rabbit(); // animal
    console.log(rabbit.name); // rabbit
    

    对于基类(Animal)的属性初始化将发生在构造函数调用前,所以构造函数调用的时候能拿到 this.name(new 执行Animal() 打印“animal”),否则获取到的 this.name 为 undefined;而在子类 Rabbit 的属性初始化过程中,会首先初始化父类(Animal)的属性,然后调用其(父类)构造函数(所以 Rabbit 父类构造函数中打印的 this.name 为 “animal”),最后才初始化自己的属性。此时如果要插入super,而又不影响原有的初始化顺序的话,需要在 this 对象属性赋值前调用super。在类的多层级继承中

    class A {
        name = 'A';
        speak() {
            console.log(this.name);
            console.log('A speak');
        }
    }
    class B extends A {
        name = 'B'
        speak() {
            super.speak();
            console.log('B speak');
        }
    }    
    class C extends B {
        name = 'C'
        speak() {
            super.speak();
            console.log('C speak');
        }
    }
    (new C()).speak();
    

    执行后得到如下结果:

    image.png
    如果使用对象的原型链来实现这种多继承的话:

    const a = {
        name: 'A',
        speak() {
            console.log(this.name);
            console.log('A speak');
        }
    }
    
    const b = {
        name: 'B',
        speak() {
            a.speak();
            console.log('B speak');
        }
    }
    
    const c = {
        name: 'C',
        speak() {
            b.speak();
            console.log('C speak');
        }
    }
    
    c.speak();
    

    只要 c.speak() 调用时打印出的结果和用extends实现继承执行的结果相同即可,同时在a、b、c的方法调用里面不应出现其他的对象(如在b的speak中出现a.speak()),可以通过bindcallapply的方式实现,添加原型和修改this指向后的代码如下:

    const a = {
        name: 'A',
        speak() {
            console.log(this.name);
            console.log('A speak');
        }
    }
    
    const b = {
        name: 'B',
        speak() {
            Object.getPrototypeOf(this).speak.call(this);
            console.log('B speak');
        }
    }
    
    const c = {
        name: 'C',
        speak() {
            Object.getPrototypeOf(this).speak.call(this);
            console.log('C speak');
        }
    }
    Object.setPrototypeOf(c, b); // c 的原型是 b
    Object.setPrototypeOf(b, a); // b 的原型是 a
    c.speak();
    

    执行发现上述代码出现了死循环,但是“逻辑上并没有问题”;实际上在对象 c 的 speak 中 Object.getPrototypeOf(this).speak.call(this) 将自身传递给了自己的基类,所以在基类 b 的 speak 函数中 中的 this 其实是 c,所以在 b 对象 的 speak 函数中相当于是调用了Object.getPrototypeOf(c).speak.call(c),和 c 对象中的 speak 函数形成了循环,而 b 对象中的 speak 函数想做的其实是调用它基类的speak函数,并将自己它获取到的 this 传递过去。显示的在每个speak函数中增加一个记录自己环境的变量就可以解决(本应该是this,但是通过call、bind、apply会改变this)

    const a = {
        name: 'A',
        speak() {
            const environmentRecord = a;
            console.log(this.name);
            console.log('A speak');
        }
    }
    
    const b = {
        name: 'B',
        speak() {
            const environmentRecord = b;
            Object.getPrototypeOf(environmentRecord).speak.call(this);
            console.log('B speak');
        }
    }
    
    const c = {
        name: 'C',
        speak() {
            const environmentRecord = c;
            Object.getPrototypeOf(environmentRecord).speak.call(this);
            console.log('C speak');
        }
    }
    Object.setPrototypeOf(c, b); // c 的原型是 b
    Object.setPrototypeOf(b, a); // b 的原型是 a
    c.speak();
    

    上述代码就实现了super的功能,但是每个对象的函数里面有一段记录自身环境的代码,为了提供解决方法,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]。我们实现的 environmentRecord 和此类似,而调用super.speak()时相当于是 Object.getPrototypeOf(environmentRecord).speak.call(this)。详细参考ECMAScript® 2015 Language Specification中的[[HomeObject]]

  • js中的类存在静态属性和静态方法,它们不属于任何对象而是类的本身(没有在函数的原型上)

    class A {
        constructor(name) {
            this.name = name;
        }
        static create(name) {
            return new this(name);
        }
        getName() {
            return this.name;
        }
    }
    
    const a1 = new A('a1');
    const a2 = A.create('a2');
    console.log(a1.getName(), a2.getName()); // "a1" "a2"
    

    image.png

    不仅常规属性和方法可以继承,静态属性和方法也可以继承

    class A {
        constructor(name) {
            this.name = name;
        }
        static create(name) {
            return new this(name);
        }
        static type = 'static property';
        getName() {
            return this.name;
        }
    }
    
    class B extends A {}
    const b1 = new B('b1');
    const b2 = B.create('b2');
    console.log(B.type, b1.getName(), b2.getName()); // "static property" "b1" "b2"
    

    在对象创建的过程中,普通的方法被创建在函数的prototype属性中,当通过new创建对象时,对象的原型指向函数的prototype属性;在继承过程当中,子类的prototype属性(本身是一个对象)的原型指向基类的prototype属性,实现了普通函数的继承;子类和基类本身是函数也是对象,所以静态函数和静态属性就挂载在对象中,继承时子类的原型指向基类,就实现了静态属性和静态方法的继承

    class A {
        constructor(name) {
            this.name = name;
        }
        static create(name) {
            return new this(name);
        }
        static type = 'static property';
        getName() {
            return this.name;
        }
    }
    const a = new A('a');
    console.log(Object.getPrototypeOf(a) === A.prototype); // true
    
    class B extends A {}
    console.log(Object.getPrototypeOf(B.prototype) === A.prototype); // true
    console.log(Object.getPrototypeOf(B) === A); // true