前端笔记--JS--20200603

133 阅读10分钟

参考部分网上的文章,总结笔记

一. JavaScript篇

1. 数据类型

JavaScript有几种数据类型:

  • 基本数据类型: undefine/null/ boolean/ number/string/symbol(es6的新数据类型)
  • 引用数据类型: object/array/function (统称为object)

数据类型检测:

  • typeof : 对于基本数据类型,除了null外,都可以正常检测出数据类型,对于对象来说,除了function外,都会显示object
typeof 5 // 'number'
typeof '5' // 'string'
typeof undefined // 'undefined'
typeof false// 'boolean'
typeof Symbol() // 'symbol'
typeof null  //object
typeof NaN   //number

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
  • instanceof: 通过原型链来判断数据类型的
p1 = new Person()
p1 instanceof Person // true
  • Object.prototype.toString.call()可以检测所有的数据类型,算是一个比较完美的方法了。
var obj={}
var arr=[]
console.log(Object.prototype.toString.call(obj))    //[object Object]
console.log(Object.prototype.toString.call(arr))    //[object Array]

2. 拷贝

对于一维对象来说,拷贝没有区别,但是对与多维度对象来说,拷贝的只是一维的数据,多为的数据并没有真正的拷贝成功。实际上,浅拷贝只拷贝一次指针,并没有拷贝一份内存地址

  • 浅拷贝: Object.assign() //es6的方法
var obj={aa:1,b:{item:'45'}};
var newObj=Object.assign({},obj);
obj.aa=2;
obj.b.item='kk';
console.log(newObj.aa);        //1
console.log(newObj.b.item);    //kk

当原对象的数据发生变化后,拷贝的对象也发生了改变,一维度的数据其实实现了深拷贝,但是多维度的数据没有实现

  • 深拷贝: JSON.parse(JSON.stringify(obj))

利用JSON.stringify(obj)将对象先转为json字符串,再JSON.parse()转回为json对象可以实现深拷贝,这也是比较常用的一种方法

  • 用js实现一个深拷贝函数:

其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

function deepCopy(obj) {
  var result = Array.isArray(obj) ? [] : {};
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        result[key] = deepCopy(obj[key]);   //递归复制
      } else {
        result[key] = obj[key];
      }
    }
  }
  return result;
}

3. 作用域和闭包

  • 变量名提升:
  1. 在 JavaScript 中,函数声明(functionaa(){})与变量声明(var)经常被 JavaScript 引擎隐式地提升到当前作用域的顶部。

  2. 函数声明的优先级高于变量,如果变量名跟函数名相同且未赋值,则函数声明会覆盖变量声明

  3. 声明语句中的赋值部分并不会被提升,只有变量的名称被提升

  • 作用域链: 因为函数的嵌套后,作用域就有了层级关系,当函数执行的时候,就会在当前的作于环境下查找对应的变量,当当前的作用域下没有这个变量,就会向上级作用域查找,直到全局作用域下,这个动作被称为作用域链。

==注意: 全局作用域下无法获取局部作用域的变量,但是局部作用域下可以访问全局作用域的变量==。

闭包(Closure):

什么是闭包: 简而言之就是一个函数可以访问其他函数内部的变量,就是这个函数和这个变量组成的闭包。

闭包的作用: 隐藏一个变量,让一个变量不能直接被访问,也即使将变量放在一个函数内部。

那么问题来了,由于作用域的关系,全局作用域下无法访问到函数内部的作用域的变量,为解决这一问题,就出现了闭包。

function foo(){
  var local = 1
  function bar(){   // bar()函数和变量local组成了一个闭包
    local++ 
    return local
  }
  return bar  // 注意 这里返回的是一个函数名,而不是一个函数
}

var func = foo()
console.log(func()) // 2
console.log(func()) // 3

闭包的优缺点:

优点:

  • 可以让外部函数访问到另一个函数内部的变量
  • 让一个变量长期保存在内存中,是的变量的生命周期变长

缺点:

  • 因为变量可以长期存在在内存中,无疑消耗了内存
  • 在ie浏览器中还会导致内存泄漏

4. 原型和继承

什么是原型: 在每一个js实例对象被创建的时候,都会关联到另一个对象,这个对象我们称之为 原型。实例对象在被创建的时候会继承原型中的属性。

JS创建一个对象的方式:

  • 对象字面量方式:
var obj = {}
  • new一个构造函数
function Pel() { }
var p = new Pel();
p.name = "hu";
p.age = "25";
delete p.name; // 删除一个属性
p.address = function () {}
console.log(p)// {name: "hu", age: "25", address: ƒ}

JS实现一个类:

  • 构造函数法:

缺点:用到了 this 和 prototype,编写复杂,可读性差

function P(name, age){
     this.name = name;
     this.age= age;
   }
   P.prototype.sal= function(){
      
   }
   var pel= new P("jj", 1);
   pel.sell()
  • ES6 语法糖 class
class Point {
       constructor(x, y) {
         this.x = x;
         this.y = y;
       }
       toString() {
         return '(' + this.x + ', ' + this.y + ')';
       }
     }
  var point = new Point(2, 3);

原型链:

  • 什么是原型链:

遍历一个实列的属性时,先遍历实列对象上的属性,再遍历它的原型对象,一直遍历到Object

任何一个类(函数)都有原型对象,原型对象至少有两个属性(constructor,proto)。constructor指向函数本身,proto指向父类原型对象。

函数上有一个prototype属性,指向原型对象,通过它可以访问原型对象

function Dog() { }        //类         
    var obj = new Dog();      //实列
    obj.name = '沪江';
    Dog.prototype.name = "旺财";
    Dog.prototype.eat = function () {
      console.log(this.name);
    };
    console.log(Dog.prototype.name);  //旺财
    console.log(obj.prototype);      //undefined,prototype是类上才有的,实列上没有
    console.log(obj.__proto__);      //{name: "旺财", eat: ƒ, constructor: ƒ}
    obj.eat();  //沪江(先遍历实列对象上的属性,再遍历它的原型对象)

Js如何实现继承?

  • 构造函数绑定:使用 call 或 apply 方法,将父对象的构造函数绑定在子对象上
function Cat(name,color){
  Animal.apply(this, arguments);
  this.name = name;
  this.color = color;
}
  • 实例继承:将子对象的 prototype 指向父对象的一个实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
  • 拷贝继承:如果把父对象的所有属性和方法,拷贝进子对象
function extend(Child, Parent) {
   var p = Parent.prototype;
   var c = Child.prototype;
   for (var i in p) {
      c[i] = p[i];
   }
   c.uber = p;
}
  • 原型继承:将子对象的 prototype 指向父对象的 prototype
function extend(Child, Parent) {
    var F = function(){};
     F.prototype = Parent.prototype;
     Child.prototype = new F();
     Child.prototype.constructor = Child;
     Child.uber = Parent.prototype;
}
  • ES6 语法糖 extends继承
class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
    }
    toString() {
        return this.color + ' ' + super.toString(); // 调用父类的toString()
    }
}

5. new和this

当我们new一个数据的时候,new操作符到底做了什么?

  • 创建一个实例对象
  • 使this指向了这个实例对象
  • 继承了构造函数的原型,以及原型的属性

如何改变this的指向

  • apply

apply方法让我们构建一个参数数组给调用函数,它也允许我们选择this的值,apply接受两个参数,第一个是要绑定给this的值,第二个就是一个参数数组。当第一个参数为null、undefined的时候,默认指向window

var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89
  • 举一个例子:
var xw={
    name: "小王",
    gender: "男",
    age: 24,
    say: function(){
        alert(this.name+" , "+this.gender+" ,今年"+this.age);
    }
}
var xh={
    name: "小红",
    gender: "女",
    age: 18
}
xw.say(); // 小王 , 男 , 今年24

那么如何用xw的say方法来显示xh的数据呢

// 将this的指向了xh
xw.say.call(xh);  
xw.say.apply(xh);
xw.say.bind(xh)();

call 和 apply的用法相似,不同的是call的其它参数是一个个单个变量,而 apply的第二个参数可以是一个数组

function fruits() {}

fruits.prototype = {
	color: 'red',
	say: function() {
		console.log(this.color);
	}
};

var apple = new fruits();

apple.say();   // red, 此时方法里面的this 指的是fruits

banana = {color: 'yellow'};
apple.say.call(banana); //yellow,此时的this的指向已经通过call()方法改变了,指向的是banana,this.color就是banana.color='yellow';

apple.say.apply(banana);//yellow,同理,此时的this的指向已经通过apply()方法改变了,指向的是banana,this.color就是banana.color ='yellow';

apple.say.apply(null); //undefined, null是window下的,此时,this 就指向了window ,但是window下并没有clolr这个属性,因此this.clolr就是window.color=undefined;

6. promise、generator、async/await怎么使用,有什么区别?

众所周知,js是一门单线程语言,如果没有异步编程,那就会导致程序卡死。

promise、generator、async/await就是异步编程的三种解决方案。

  • 回调函数

在以上三种出现之前,js通过回调函数来时先异步操作;所谓回调函数(callback),就是把任务分成两步完成,第二步单独写在一个函数里面,等到重新执行这个任务时,就直接调用这个函数。并且容易出现回调地狱

  • promise:

假定我们有一个需求,读取完A文件之后读取B文件,再读取C文件,代码如下:

fs.readFile(fileA,  (err, data) => {
  fs.readFile(fileB,  (err, data) => {
      fs.readFile(fileC, (err,data)=>{
        //do something
    })
  });
});

primise不是一种新的功能,而是一种新的写法:

function doIt() {
    console.time('do it now')
    const time1 = 300;
    step1(time1)
          .then( time2 =>step2(time2))
          .then( time3 => step3(time3))
          .then( result => {
              console.log(`result is ${result}`)
           });
}

doIt();
  • Generator函数:

Generator是协程在ES6的实现,最大的特点就是可以交出函数的执行权,懂得退让。

function* gen(x) {
    var y = yield x +2;
    return y;
  }
  
  var g = gen(1);
  console.log( g.next()) // { value: 3, done: false }
  console.log( g.next()) // { value: undefined, done: true }
 

上面代码中,函数多了*号,用来表示这是一个Generator函数,和普通函数不一样,不同之处在于执行它不会返回结果,

返回的是指针对象g,这个指针g有个next方法,调用它会执行异步任务的第一步。 对象中有两个值,value和done,value 属性是 yield 语句后面表达式的值,表示当前阶段的值,done表示是否Generator函数是否执行完毕

下面看看Generator函数如何执行一个真实的异步任务:

var fetch = require('node-fetch')

function* gen(){
  var url = 'https://api.github.com/users/github'
  var result = yield fetch(url)
  console.log(result.bio)
}

var g = gen()
var result = g.next()

result.value.then( data => return data.json).then (data => g.next(data))
  • Async/await

从回调函数,到Promise对象,再到Generator函数,JavaScript异步编程解决方案历程可谓辛酸,终于到了Async/await。很多人认为它是异步操作的最终解决方案

其实async函数就是Generator函数的语法糖,例如下面两个代码:

var gen = function* (){
  var f1 = yield readFile('./a.txt')
  var f2 = yield readFile('./b.txt')
  console.log(f1.toString())
  console.log(f2.toString())
}
var asyncReadFile = async function (){
  var f1 = await  readFile('./a.txt')
  var f2 = await  readFile('./b.txt')
  console.log(f1.toString())
  console.log(f2.toString())
}

async function doIt() {
    console.time("doIt")
    const time1 = 300;
    const time2 = await step1(time1)
    const time3 = await step2(time2)
    const result = await step3(time3)
    console.log(`result is ${result}`)
    console.timeEnd("doIt")
}

doIt();

7. Event Loop(事件循环)

事件循环

image
image

  • 堆,栈、队列

(Heap)是一种数据结构,是利用完全二叉树维护的一组数据。

(Stack)是只能表尾进行插入或删除操作的线性表,它按照后进先出的原则存储数据

队列(Queue)是只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作的线性表。故队列又称为先进先出

setTimeout(function () {
    console.log('1')
  });

  new Promise(function (resolve) {
    console.log('2');  // 同步任务进入主线程
    resolve();
  }).then(function () {
    console.log('3')
  });

  console.log('4');  // 同步任务进入主线程
// 2,4,3,1

8. 斐波那契数列

// 闭包法
function fib() {
    var arr = [0, 1]
    return f = function (n) {
      var res = arr[n]
      if (typeof res !== 'number') {
        arr[n] = f(n - 1) + f(n - 2)
        res = arr[n]
      }
      return res
    } 
  }
  let p = fib()
  p(10)
  console.log(p(10)) //55 

9. 数组去重

// 方法一:
function uniq(array){
    var temp = []; //一个新的临时数组
    for(var i = 0; i < array.length; i++){
        if(temp.indexOf(array[i]) == -1){
            temp.push(array[i]);
        }
    }
    return temp;
}

var aa = [1,2,2,4,9,6,7,5,2,3,5,6,5];
console.log(uniq(aa));

// 方法二:
function uniq(array){
    var temp = [];
    var index = [];
    var l = array.length;
    for(var i = 0; i < l; i++) {
        for(var j = i + 1; j < l; j++){
            if (array[i] === array[j]){
                i++;
                j = i;
            }
        }
        temp.push(array[i]);
        index.push(i);
    }
    console.log(index);
    return temp;
}

var aa = [1,2,2,3,5,3,6,5];
console.log(uniq(aa));

10. 事件冒泡,和事件捕获

事件冒泡和事件捕获分别是微软和网景公司提出的,正对页面中的事件流发生的顺序问题

如下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="div">
    点击了div
    <p id="p">点击了p</p>
  </div>
  <script>
    let div = document.getElementById('div')
    let p = document.getElementById('p')
    // div.onclick = function () {
    //   console.log('触发了div的事件')
    // }
    // p.onclick = function () {
    //   console.log('触发了p的事件')
    // }
    div.addEventListener('click', function () {
      console.log('触发了div的事件')
    },true)
    p.addEventListener('click', function () {
      console.log('触发了p的事件')
    })
  </script>
</body>

</html>

当一个div内部套了一个p标签,并且两者都绑定了各自的事件,当我们点击p标签的时候,会先触发哪一个事件,就是这个问题引发了。

事件冒泡: 事件冒泡 认为 事件会从底部开始向外扩散事件,也就是先出发内部p标签绑定是的事件函数,再触发外部的div事件函数。

事件捕获: 事件捕获 认为 外部的div会先捕获到用户点击的事件,所以div会先触发事件函数,在触发内部p标签的事件函数,与冒泡事件 恰好相反。

最后在W3C的联盟干预下,采用这种的方法,也就是在事件监听的函数addEventListener加入第三个参数,默认为false,代表采用冒泡事件处理机制,true则代表采用事件捕获的处理机制。