我的前端知识梳理-ES5篇

1,156 阅读14分钟

本篇文章记录自己对es5知识点的理解

======================相关文章和开源库=======================

系列文章

1.前端知识梳理-HTML,CSS篇

2.前端知识梳理-ES5篇

3.前端知识梳理-ES6篇

4.前端知识梳理-VUE篇

个人维护的开源组件库

1.bin-ui,一个基于vue的pc端组件库

2.树形组织结构组件

3.bin-admin ,基于bin-ui的后台管理系统集成方案

4.bin-data ,基于bin-ui和echarts的数据可视化框架

5.其余生态链接

========================================================


1 JS创建对象的方式

es5有三种方式创建对象,分别是

// 第一种方式,字面量var
o1 = {name: "o1"}var
o2 = new Object({name: "o2"})// 第二种方式,通过构造函数var
M = function(name){ this.name = name }var
o3 = new M("o3")// 第三种方式,Object.createvar  p = {name: "p"}var
o4 = Object.create(p) 对于对象,在这里也不做赘述,这里解释一下js中什么是标识符

/*

标识符

- 在JS中所有的可以由我们自主命名的都可以称为是标识符

- 例如:变量名、函数名、属性名都属于标识符

- 命名一个标识符时需要遵守如下的规则:

  1.标识符中可以含有字母 、数字 、下划线_ 、$符号

  2.标识符不能以数字开头

  3.标识符不能是ES中的关键字或保留字

  4.标识符一般都采用驼峰命名法

- JS底层保存标识符时实际上是采用的Unicode编码,

所以理论上讲,所有的utf-8中含有的内容都可以作为标识符

*/

这里就看出js的缺陷了,如果对象中的属性不符合标识符规范怎么办,也就是我用.操作符无法获取属性的时候,比如说  var obj={ 1 : 1 } console.log(obj.1)

事实上我们是无法通过点操作符来获取这个属性的,这里我们只能用var obj={ ‘1’:1 } 来声明对象,那该如何获取属性呢,js为我们提供了一个类似数组索引 [ ] 的方式来获取对象

如上面的 var obj={name:’张三’}

我们可以用obj.name 或者 obj[‘name’] 这两种方式来获取 但是var obj={ ‘1’:1 } 只能用obj[‘1’]来获取这个属性值

那这两种有什区别呢

. 运算符:右侧必须是一个属性名称命名的简单标识符,如.name

[]:右侧必须是一个计算结果为字符串的表达式

那既然[]是一个计算结果为字符串的表达式,那就给[]赋予了强大的功能,如字符串拼接,三目运算符等

let obj={'1':123, name:'张三'}
console.log(obj['1'],obj.name,obj['name']) // 输出 123 "张三" "张三"

2 ==和===的区别

不同于Java中的比较,js有这两种比较的方式,但他们有什么区别呢

简单来说: == 代表相同, ===代表严格相同, 为啥这么说呢

这么理解: 当进行双等号比较时候: 先检查两个操作数数据类型,如果相同, 则进行===比较, 如果不同, 则愿意为你进行一次类型转换, 转换成相同类型后再进行比较, 而===比较时, 如果类型不同,直接就是false.

比较过程:

  双等号==:

  (1)如果两个值类型相同,再进行三个等号(===)的比较

  (2)如果两个值类型不同,也有可能相等,需根据以下规则进行类型转换在比较:

    1)如果一个是null,一个是undefined,那么相等

    2)如果一个是字符串,一个是数值,把字符串转换成数值之后再进行比较

  三等号===:

  (1)如果类型不同,就一定不相等

  (2)如果两个都是数值,并且是同一个值,那么相等;如果其中至少一个是NaN,那么不相等。(判断一个值是否是NaN,只能使用isNaN( ) 来判断)

  (3)如果两个都是字符串,每个位置的字符都一样,那么相等,否则不相等。

  (4)如果两个值都是true,或是false,那么相等

  (5)如果两个值都引用同一个对象或是函数,那么相等,否则不相等

  (6)如果两个值都是null,或是undefined,那么相等

那什么时候用==什么时候用===呢,我个人认为的是

只有在判断obj.a==null的时候使用双等,等同于obj.a===undefind||obj.a===null

其他一律使用===(三等号)因为我们通常在判断是否相等的时候首先就得确定两遍的类型


3 为什么 0.1 + 0.2 != 0.3

先来看一个现象,那么为什么会出现这种情况呢


因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。

我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为

// (0011) 表示循环

0.1 = 2^-4 * 1.10011(0011)

这个问题简单来说是底层在表示0.1和0.2的时候采用的是二进制,在采用这个双精度版本时,二进制相加后再转换为十进制的时候就会得到结果是不等于0.3的

那么我们实际编码时如果遇到这种情况该怎么判断呢

原生的解决办法如下

parseFloat((0.1 + 0.2).toFixed(10))或者转换成整数计算后再转换成小数

4 什么是原型和原型链

每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。

对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。

构造函数

function Foo(name,age){
    this.name = name;
    this.age = age;
    //return this;默认有这一行
}
var f = new Foo('zhangshan',20);
//1.创建了一个新对象
//2.将this指向这个新对象
//3.执行构造函数里面的代码
//4.返回新对象(this)

var a={} //其实是var a = new Object()语法糖
var a=[] //其实是var a = new Array()语法糖
function Foo(){} // 其实是var Foo = new Function()

// 使用instanceof判断一个函数是否是一个变量的构造函数
// f instanceof Foo  ==> true
// f.__proto__一层层往上寻找能否查询到Foo.prototype
// f instanceof Object  ==> true

原型的规则和示例

// 1.所有的引用类型(数组,对象,函数),都具有对象特性,即可以自由扩展属性(null除外)  
    var obj={};obj.a=100;
    var arr={};arr.a=100;
    function fn(){} 
    fn.a=100;
// 2.所有的引用类型(数组,对象,函数),都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
    console.log(obj.__proto__);
    console.log(arr.__proto__);
    console.log(fn.__proto__);
// 3.所有的函数,都有一个prototype属性(显式原型),属性值是一个普通的对象
    console.log(fn.prototype);
// 4.所有的引用类型(数组,对象,函数),__proto__属性指向它的构造函数的prototype属性值
    console.log(obj.__proto__===Object.prototype);
// 5.当视图得到一个对象的某个属性的时候,如果这个对象本身没有这个属性,那么
去它的__proto__(即它的构造函数prototype)中去找
    function Foo(name,age){
        this.name = name;
    }
    Foo.prototype.alertName = function(){
        alert(this.name);
    }
    //创建实例
    var f = new Foo('zhangsan');
    f.printName = function(){
        console.log(this.name);
    }
    f.printName();//可以获取到printName这个属性直接获取
    f.alertName();//获取不到alertName属性这是去找他的构造函数的prototype中去找

原型链

var f = new Foo('zhangsan');
f.toString();

分析:

1.首先去自身的属性中去找toString方法,没有则去隐式原型__proto__去找

2.__proto__指向其构造函数的显式原型Foo.prototype,如没找到再去Foo.prototype的隐式原型去找

3.Foo.prototype__proto__又指向Object.prototype显式原型,然后再寻找其隐式原型__proto__即查询到toString方法,这就是原型链

总结

Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它 Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它 Function.prototypeObject.prototype 是两个特殊的对象,他们由引擎来创建 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的 函数的 prototype 是一个对象,也就是原型 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链

5 什么是作用域和闭包

作用域和闭包是一个老生常谈的问题,经常会出现在各大面试中。下面就来简单介绍一下,

执行上下文

// 示例代码:
    console.log(a);//undefined
    var a=100;
    fn('zhangsan');// zhangsan 20
    function fn(name){
        age=20;
        console.log(name,age);
        var age;
    }
// 解释:
// 范围 | 一段<script>标签或者一个函数
// 全局 | 变量定义,函数声明
// 函数 | 变量定义,函数声明,this,arguments  

this的指向

this要在执行时才能确认,定义时无法确认,使用bind的时候必须要使用函数表达式

// 示例代码:
    var a = {
        name:'A',
        fn:function(){
            console.log(this.name);
        }
    }
    a.fn();//this===a
    a.fn.call({name:'B'});//this==={name:'B'}
    var fn1 = a.fn;
    fn1();//this===window
// 分析:
// 1.作为构造函数执行
// 2.作为对象属性执行
// 3.作为普通函数执行
// 4.call apply bind
    function fn1(name){
        alert(name);
        console.log(this);
    }
    fn1.call({x:100},'zhangsan');//call第一个参数就是this
    // 执行后弹出'zhangsan' 打印{x:100}
    var fn2 = function (name){
        alert(name);
        console.log(this);
    }.bind({y:200})
    fn2('zhangsan');
    // 执行后弹出'zhangsan' 打印{y:200}
作用域
// 1.没有块级作用域
    if(true){
        var name = 'zhangsan';
    }
    console.log(name);

// 2.有函数和全局作用域
    var a = 100;
    function fn(){
        var a = 200;
        console.log('fn',a);
    }
    console.log('global',a);
    fn();

// 3.自由变量
    var a = 100;
    function fn(){
        var b = 200;
        console.log(a);//当前作用域没有定义的变量叫做自由变量
        console.log(b);
    }
// 注:这里打印a这个自由变量,则会去它的父级作用域去查询a,所谓的父级作用域就
是在函数定义的时候的作用域

// 4.作用域链
    var a = 100;
    function F1(){
        var b = 200;
        function F2(){
            var c = 300;
            console.log(a);//a是自由变量,先去父级F1中找a,没有再去F1的父级作用域找
            console.log(b);//b是自由变量,先去父级F1中找b,找到后打印
            console.log(c);
        }
        F2();
    }
    F1();
// 注:F2的父级作用域是F1,F1的父级作用域是全局,这就是作用域链

什么是闭包

闭包的定义很简单:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

经典面试题,循环中使用闭包解决 var 定义函数的问题

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

到这里我们插入两个常见的问题写出以下程序的输出结结果

var a = 6;
setTimeout(function () {
    console.log(a);
    a = 666;
}, 1000); 
setTimeout(function () {
    console.log(a);
    a = 777;
}, 0);

a = 66;

//分析
// 1.执行第一行a=6
// 2.执行setTimeout后,传入其中的函数会被暂存不会立即执行
// 3.执行最后一行a=66
// 4.等待所有程序执行完,处于空闲状态时,立即查看有没有暂存的执行队列
// 5.发现暂存的执行函数,如没有等待时间则立即执行,即第二个setTimeout,输出a=66,执行a=777
// 6.暂存的执行函数有等待时间的,1秒后输出a=777,再给a赋值666,因此会先输出66,1秒后输出777

请输出以下程序的结果,并简单分析一下过程

代码1:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    return function(){
       return this.name;
    };
   }
  };
console.log(object.getNameFunc()());

代码2:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    var that = this;
    return function(){
      return that.name;
    };
  }
};
console.log(object.getNameFunc()());

// 分别输出 The Window, My Object

// 1.object.getNameFunc()返回一个函数,再调用()执行的时候上下文this为全局。

// 2. object.getNameFunc()返回一个函数的时候执行了that=this;,这时候that缓存的是上下文的this。即为 object,再调用()执行方法的时候打印的就是object的name

6 什么是防抖和节流

在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次, 而节流的 情况会每隔一定时间(参数wait)调用函数。

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

袖珍版的防抖理解一下防抖的实现:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,
并不会执行函数

这里只是简单说明一下这个概念,节流函数的实现,感兴趣的可以尝试自己写一下。

7 深浅拷贝

let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。

通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

浅拷贝

首先可以通过 Object.assign 来解决这个问题。

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

当然我们也可以通过展开运算符(…)来解决

let a = {
    age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始

深拷贝

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'wb'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "wb"}

你会发现在上述情况中,该方法会忽略掉函数和 undefined

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用递归来实现一个深拷贝函数(此部分后续会在vue篇进行实现),为实现深拷贝函数,这里再引入一个知识点

8 如何精准判断一个类型

我们知道可以使用typeof函数来判断一个类型,但是使用typeof判断确不是很准确,如下

那么如果我要精确判断一个类型是对象还是数组,或者是函数该如何实现呢?

这里我们可以使用Object.prototype.toString.call(obj) 来精确判断类型

function typeOf (obj) {
  const  toString = Object.prototype.toString
  const  map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object'
  }
  return map[toString.call(obj)]
}