JS难点深入学习

301 阅读10分钟

1.typeof 检测数据类型

type of检测时返回值是字符串,是对应数据类型,检测到null时返回的是object,检测function和Array返回的是function.因为object里面包括有call,就会返回function,不然是正常返回object。如果变量赋值的时候用的是new出来的对象,那么typeof结果就是object

2.instanceof检测输出的是bool

检测是A是否B实例化对象,A instanceof B,[]对应Array,{}对应Object,[]也可以对应Object,因为这是instanceof原型链。new出来的都可以对应Object或构造函数调用的类,如new Date() instanceof Object结果是true。可以用Object.prototype.toString.call()来检测想要的某个数据的类型,也可以用obj.constructor来判断该对象的构造函数是什么类型的,返回类型是数据类型,不是字符串形式。

3.js的堆栈

栈是为原始类型开辟的内存空间,堆是为引用类型开辟的内存空间。假设一开始变量定义的是一个对象,那该对象开辟的栈中存的是堆的地址,堆中存的是数据,所以将该变量赋值给其他变量,改变值后,是堆中数据改变了,那么所有变量是该地址的数据都会改变

4.快速浅拷贝

B复制得A,修改A,B变化是浅拷贝,B不变时深拷贝。核心是创建一个新对象,该对象有原始对象属性的拷贝,对基础数据类型直接拷贝值,对引用类型(属性是内存地址)仅拷贝第一层对象的属性(内存地址),所以其中一个对象改变地址会影响到另一个对象。 浅拷贝:var objcopy=Object.create(obj);var objcopy=Object.assign(obj,source);//用于js对象合并,参数1是目标对象,参数2是来源对象。扩展运算符完成浅拷贝。数组的concat和slice也是浅拷贝。

5.快速深拷贝

var objCopy=JSON.parse(JSON.stringfy(obj))//stringfy创建新内存把object转JSON格式字符串,parse解析JSON字符串并构造对象

缺点是会忽略undefined,symbol,不能正确拷贝Function,序列化会自动删除,对于map,set,regexp,date,arraybuffer和其他内置类型进行序列化时会丢失,不支持循环引用对象拷贝。

1)普通方法

可以用Object.assign,默认是深拷贝,但只针对最外层,对象内嵌套对象时还是浅拷贝,数组拷贝方法...,slice,concat是一个效果。可以用jQuery的extend:$.extend(true,target,obj),需要jQuery,不太常用。也可以调第三方库lodash的_.cloneDeep()。

2)特殊方法MessageChannel

MessageChannel(宏任务,VUE中优先检测是否支持原生setImmediate,不支持再检测是否支持MessageChannel),这个API允许创建一个消息通道,并通过MessagePort属性发送数据,有2个端口(只读),每个端口都可以用postMessage发送数据,一个端口绑定onMessage回调方法,就可以接受从另一个端口传来的数据。在用于深拷贝时用里面的postMessage传递的数据时深拷贝的,可、和web worker的postMessage(跨域的时window的方法)一样 ,还可以拷贝undefined和循环引用的对象。MessageChannel是异步的,不能支持拷贝有Function的对象。

function deepCopy(obj) {
      return new Promise((resolve) => {
        const {port1, port2} = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
      });
    }

    deepCopy(obj).then((copy) => {           // 请记住`MessageChannel`是异步的这个前提!
        let copyObj = copy;
        console.log(copyObj, obj)
        console.log(copyObj == obj)
    });

这个特性也可以用于实现web worker之间实现通信。web worker其实是运行在后台的js,独立于其他脚本,不影响页面的性能,js时单线程语言,H5提出web worker标准允许JS创建多个线程,作为子线程且受主线程控制,但不是不能操作DOM节点,本质还是单线程,所以不能访问window,document这种全局变量或全局函数,但是还可以用setTimeout或是XMLHttpRequest对象来做ajax通信。用MessageChannel实现web worker之间的通信代码:

<script>
    var w1 = new Worker("worker1.js");
    var w2 = new Worker("worker2.js");
    var ch = new MessageChannel();
    w1.postMessage("port1", [ch.port1]);
    w2.postMessage("port2", [ch.port2]);
    w2.onmessage = function(e) {
        console.log(e.data);
    }
</script>

// worker1.js
  onmessage = function(e) {
    const  port = e.ports[0];
    port.postMessage("this is from worker1")        
  }
  
// worker2.js
    onmessage = function(e) {
        const port = e.ports[0];
        port.onmessage = function(e) {
            postMessage(e.data)
        }
    }

3)手写深拷贝

手写方法解决循环引用和非循环的子对象拷贝的深拷贝(未处理不可遍历的值和Array等特殊对象):

function deepcopy(obj)
{
    let map=new WeakMap();
    function dp(obj){
        let result,keys,key,temp,existObj=null;
        existObj=map.get(obj);//获取obj的值
        //该对象已被记录,就直接返回
        if(existObj){ return existObj;}
        keys=Object.keys(obj)//获取obj的key数组
        result={};
        map.set(obj,result);//记录当前对象
        for(let i=0;i<keys.length;i++)
        {
            key=keys[i];
            temp=obj[key];
            if(temp && typeof temp =='object')
            { result[key]=dp(temp);}再分析里面的对象
            else
            {result[key]=temp;}//赋值给新对象
        }
        return result;
    }
    return dp(obj);
}
function deepClone(source) {
  // null数据需要特殊处理
  if (source === null) {
    return source
  }
  // 校验要拷贝的数据是对象还是数组
  const target = Array.isArray(source) ? [] : {}
  for (const k in source) {
    const val = source[k]
    const valueType = typeof val
    // 校验拷贝的数据类型
    if (valueType === 'function') {
      target[k] = new Function(`return ${val.toString()}`)()
    } else if (valueType === 'object') {
      target[k] = deepClone(val)
    } else {
      target[k] = val
    }
  }
  return target
}

const obj1 = { name: 'dog', info: { age: 3 }, fn: function () {} }
const obj2 = deepClone(obj1)
obj2.name = 'cat'
obj2.info.age = 4
console.log(obj1) // { name: 'dog', info: { age: 3 }, fn: function(){} }
console.log(obj2) // { name: 'cat', info: { age: 4 }, fn: function(){} } 

6.遍历对象的方式

1)for in

以任意顺序遍历一个对象除Symbol以外的可枚举属性,包括继承的可枚举属性

2)for of(不推荐)

把普通对象改造成可迭代对象,通过实现@@iterator是对象拥有一个带Symbol.iterator的方法

    *[Symbol.iterator]() {
        yield 'name'
        yield 'age'
    },
    name: "nordon",
    age: 12,
};

for (const key of iteratorObj) {
    console.log(key, "---", iteratorObj[key]);
}

3)Object.keys

将对象自身可枚举属性组成数组并返回,最后得到的是由可枚举key组成的数组,再遍历该数组,用forEach,然后输出obj[key] 就可以依次取出对象的数据

Object.keys(obj).forEach(key => {
  console.log(key, '---', obj[key]);
})

4)Object.entries

将对象自身可枚举属性的键值对作为数组返回,返回的是二维数组,配合for...of就可以把对象的键值取出。

for (const [key, value] of Object.entries(obj)) {
  console.log(key, '---', value);
}

6.常见数据类型转换的坑

特殊类型隐式转换:

NAN,0,undefined,null,""=>false

逻辑运算符&&和||隐式转换

    console.log(0&&5)//true &&false,会返回false的值0

==和===的区别

undefined==null,因为会做隐式转换,都变成0,所以会变成0==0,返回true,但是undefined===null,不会隐式转换,返回false.所以开发中最好都用===,!==

7.js舍入误差

0.1+0.2!==0.3,是因为浮点数精度问题,因为计算机是二进制机器码,是有规定的位数的,所以存储之后,二进制转十进制,会出现精度丢失。可以用(0.1+0.2).toFixed(2)保留2位,但一般用在比较小的数据量,也可以使用将数乘一定的倍数成为整数,进行运算后再把倍数除回去。

8.for循环性能优化

用一个变量存储长度,就不用每次读取,能够优化。

9.拆箱装箱

装箱把基本数据类型转成对应引用数据类型:var objnum=new Number(123),拆箱将引用类型对象转换为对应的值类型对象,本质是用了valueOf()方法,用来返回原始类型的值,如没有就返回对象本身。js里用toPrimitive(input,type),input传入值,type值类型,然后最后能返回原始类型。toString方法是字符串转换。console.log([]+[])输出的是‘’空字符串,其实是调用了toPrimitive方法,先判断是否原始类型,valueof,结果是[],仍不是原始类型,再toString,输出空字符串。当console.log([]+{}),就返回[object Object]。如果是console.log({}+[]),有些浏览器可能是0,因为可能把大括号识别成代码块,就只执行加号,输出值就可能是0。

10.sort排序不可靠

sort本质是对数字转成字符串后的unicode码表来排,如果遇到单位数和多位数一起,很容易有问题,所以为了解决应该定义一个比较器函数,在里面回调 sort(function(x,y){return x-y;})如果要降序排就y-x

11.编码和解码

escape()是对除了ascii表的来编码,unescape是解码。encodeURI主要对中文进行编码,decodeURI是解码

12.继承的实现

1)结合原型链和构造函数:寄生式组合继承(最简单)

Person(){
this.name=name;
};
Student(){
this.age=age;
};
Student.prototype=Object.create(Person.prototype);//创建新对象,里面装的是父类prototype的各属性
Student.prototype.constructer=Student;//定义构造器时,这个constructor指向的是构造器本身,但是继承之后constructor被修改了,这里重新赋值为原来的Student,只使用父类的属性,不使用父类的构造函数,所以新创建的Student实例又是Student作为构造函数了。

2)组合继承(盗用构造函数+原型链)调用了2次父类构造函数

Person(){
this.name=name;
};
Student(){
this.age=age;
Person.call(this,name);
};
Student.prototype=new Person();//原型属性的{}中有Person的对象及其各种属性
Student.prototype.constructer=Student;//同寄生式

3)原型链继承

Person(){
this.name=name;
};
Student(){
this.age=age;
};
Student.prototype=new Person();//会导致创建的所有实例都共享相同的引用类型属性,子类实例化不能给父类传参

4)借用构造函数实现继承

解决了原型链继承的缺点,但是由于方法必须定义在构造函数中,每次创建子类实例都创建一遍方法。

Person(){
this.name=name;
};
Student(){
this.age=age;
Person.call(this,name);
};
Student.prototype=new Person();

5)class继承

class Person{
constructer(name){
this.name=name
}
}
class Student extends Person{
construter(name,age){
super(name);
this.age=age
}
}

}

13.数组去重扁平化并排序

var arr=[];
Array.from(new Set(arr)).flat(Infinity).sort((a,b)=>a-b);
[...new Set(arr)].flat().sort();//一样的

14.实现flat使数组扁平化

function flatten(arr)
{
    if(arr.some(item=>Array.isArray(item))//存在项仍是数组,即数组是多维
    {
        [].concat(...arr);//全部展开再连起来
    }
    return arr;
}

15.判断是否循环引用

循环引用是对象内部的属性指向对象本身,判断时递归判断每个对象属性是否与源相等,可以调包用JSON.decycle()解除循环引用。

function hasLoop(obj){
    function findLoop(target,src){
        const source=src.slice().concat([target]);//这是源数组,把自身传入
        for(let key in target){
            if(typeof target[key] === 'object')
            {
                //在源数组找或递归查找内部属性
                if(source.indexOf(target[key])>-1 || findLoop(target[key],source))
                    return true;
            }
        }
        return false;
    }
    return typeof obj === 'object' ?findLoop(obj,[]):false;
}

16.new一个对象发生了什么

首先开辟了一块内存地址,创建了一个空对象,然后这个空对象的_proto_指向构造器的prototype,也就是设置原型链(create),然后改变this的指向,让this指向创建的空对象(result)。判断返回值类型,如果是引用类型,就返回这个引用类型的对象(result),如果是普通值类型,就返回创建的对象(child)。

class Lijianhua {
  constructor () {
    this.lastname = 'li'
    this.firstname = 'jianhua'
  }

  say () {
    console.log('hi')
  }
}

// 自己定义的new方法
const newMethod = function (Parent, ...rest) {
// 1.以构造器的prototype属性为原型,创建新对象;
  const child = Object.create(Parent.prototype)
  // 2.将this和调用参数传给构造器执行
  const result = Parent.apply(child, rest)
  // 3.如果构造器没有手动返回对象,则返回第一步的对象
  return typeof result === 'object' ? result : child
}
// 创建实例,将构造函数Parent与形参作为参数传入
const her = newMethod(Lijianhua)
her.say() // 'hi';
console.log(her instanceof Lijianhua) // true

17.事件委托(事件代理)

事件流是事件捕获->事件目标->事件冒泡。捕获阶段事件相应的监听函数不会被触发(除非设置好监听函数在捕获阶段触发),到达目标元素执行目标元素该事件相应的处理函数。监听子元素时,事件冒泡会通过目标元素向上传递到父级,直到document。最深的节点开始,然后逐步向上传播事件。如果子元素不确定或动态生成,可以通过监听父元素来取代监听子元素(addEventListener,第三个参数规定选择事件冒泡/事件捕获)。给父级添加事件,事件被抛到更上层的父节点时,通过检查事件的目标对象target来判断并获取事件源(target表示为当前事件操作的dom),子节点被点击的时候,事件从子节点向上冒泡,到达父节点后判断e.target.nodeName(e是事件函数的参数)是否为需要处理的节点,e.target就是子节点,当子元素中有多层嵌套,就导致事件无法被正确触发,可以对元素进行判断,看是不是外元素下的子元素,一层一层往下找。e.target.tagName判断标签名字(字符串),可以再加一层判断e.target是不是父元素,不是就设e.target=e.target.parentNode,往上移。添加子元素:appendChild。适合用事件委托的事件是所有用到按钮的事件和多数的鼠标与键盘事件。mouseover,mouseout,mousemove经常需要计算位置,就不好把控,focus,blur没用冒泡特性,不用事件委托。

18.Symbol应用场景

用Symbol作为对象属性名(key)

然后想通过Objects.key或for in来枚举对象的属性名就不会显示,所以用JSON.stringfy把对象转成JSON字符串也无法输出,可以将不需要对外操作和访问的属性用Symbol定义。使用Object.getOwnPropertySymbols()和Reflect.ownKeys()可以获取Symbol定义的对象属性。

使用Symbol代替常量

需要定义一组常量代表业务逻辑下几个不同类型,希望常量是唯一的,一般为常量赋一个唯一的值,用Synmbol()可以直接保证常量唯一。

使用Symbol定义类的私有属性/方法

在class中,用[]括起来的属性名就是Symbol类型的,可以作为私有属性

注册和获取全局Symbol

应用涉及多个window,这些window中使用的某些Symbol是同一个,就不能用Symbol()函数,可以用Symbol.for()注册或获取一个window间全局的Symbol实例,在多个相关的window间是唯一的。

const和Symbol的区别

const表示定义后不可修改或重复定义,symbol表示定义的变量值绝不重复。

定义类私有属性的方法

给私有方法加下划线_,给私有属性/方法前面加#,或者把私有方法移出模块