前端基础🎈——帮你系统化梳理JS 重点知识(稳步前进)

635 阅读24分钟

稳步前进,持续努力,处事不惊
js是前端的重中之重,以下将js的重点基础知识挑出来做了系统化整理,帮助更好地形成知识体系O(∩_∩)O~

一、js的原始数据类型和引用数据类型

原始数据类型:
Undefined、Null、Boolean、Number、String、Symbol、BigInt
引用数据类型:
Object、Array、Function、RegExp、Date、Math

解释一下Symbol?

创建的每一个Symbol实例是唯一的,具有唯一性。

解释一下BigInt?

1. 当出现非常大的数的时候js无法精确表示问题
在JS中,所有数字都以双精度64位浮点格式表示,所以js将无法精确表示那些非常大的整数。 确切地说,JS 中的 Number类型只能安全地表示-9007199254740991(-(2^53-1)) 和 9007199254740991(2^53-1)之间的整数,任何超出此范围的整数值都可能失去精度(如下)。

9007199254740992 === 9007199254740993;    // 输出true,显然不对 

2. BigInt创建方法
法一:

console.log( 9007199254740995n );    // 输出 9007199254740995n

法二:

BigInt("9007199254740995");    // 输出 9007199254740995n

3. 利用BigInt解决数据不精确问题

console.log(9007199254740995n === 9007199254740993n);    // 输出false,正确
console.log(BigInt("9007199254740992") === BigInt("9007199254740993"));    // 输出false,正确

4. BigInt目前兼容性还不够好~

二、js数据类型判断

1. typeof

console.log(typeof 2);               // 'number'  √
console.log(typeof 'aaa')            // 'string'  √
console.log(typeof undefined)        // 'undefined'  √
console.log(typeof true)             // 'boolean'  √
console.log(typeof Symbol())         // 'symbol'  √
console.log(typeof BigInt('1'))      // 'bigint'  √
console.log(typeof null);           // 'object'   ×

typeof 主要用于判断基本类型,除了null都可以显示正确的类型,虽然null是基本类型,但是会显示object,这是一个存在了很久的bug。

2. instanceof

console.log([] instanceof Array);          //true
console.log({} instanceof   Object);       //true
console.log(/\d/ instanceof RegExp);       //true
<!--下面function就不够准确-->
console.log(function(){} instanceof Object);//true
console.log(function(){} instanceof Function);//true

instanceof主要用于判断引用数据类型的Array,Object和RegExp; function和有些基本类型不能被精准判断。

顺便说一下,有关null的知识点

console.log(null instanceof Object);          //false

typeof null是出了一个bug,就是返回"object"这个字符串。

但是本质上Null和Object不是一个数据类型,null值并不是以Object为原型创建出来的。所以null instanceof Object是false。

3. constructor

console.log(("1").constructor === String);            //true
console.log((1).constructor === Number);              //true
console.log((true).constructor === Boolean);         //true
console.log((function() {}).constructor === Function);  //true
console.log(({}).constructor === Object);            //true
// console.log((null).constructor === Null);       报错
// console.log((undefined).constructor === Undefined);   报错

constructor检测null和undefined会报错,因为它们是无效对象,不存在constructor这个属性;
constructor还有一个问题:

在说问题之前再介绍一下constructor,
当一个构造函数Fn被创建时,js引擎会为其添加prototype原型,并在prototype原型对象上添加constructor属性,并让其指向构造函数Fn的引用。总之,constructor主要用于记录该对象引用于哪个构造函数。

正常情况下:

function Fn() {

};
console.log(Fn.prototype.constructor === Fn); //true
let f = new Fn();
console.log(f.constructor === Fn); //true 实例对象f的constructor指向Fn

但当我们重写了Fn的prototype之后,原有的constructor会丢失,此时Fn的实例对象f的constructor也不会再指向Fn,这时候的constructor会默认指向Object。

有问题的情况下:

function Fn() {

};
Fn.prototype = {};
let f = new Fn();
console.log(f.constructor === Fn); //false
console.log(f.constructor === Object); //true

也就是说本来正常情况下,实例对象f的构造函数应该是Fn,但重写Fn的prototype后,constructor却错指为object。

解决方法:
在重写对象原型时一般都需要重新给constructor赋值,以保证实例对象的类型不被改写。

function Fn() {

};
Fn.prototype = {};  //prototype被改写
let f = new Fn();
console.log(f.constructor === Fn); //false
f.constructor = Fn; //重新给constructor赋值
console.log(f.constructor === Fn); //true
console.log(f.constructor === Object); //false

所以constructor在重写构造函数的prototype情况下会出现判断不准的问题,解决方法是重新给constructor赋值。

4. Object.prototype.toString.call()

这是jQuery源码里面判断类型的方法,所有数据类型都能判断出来。

var a = Object.prototype.toString;

console.log(a.call("aaa"));   // '[object String]'
console.log(a.call(1));
console.log(a.call(true));
console.log(a.call(null));
console.log(a.call(undefined));
console.log(a.call([]));
console.log(a.call(function() {}));
console.log(a.call({}));

三、==, === 和 Object.is 有什么区别?

1)==通过类型转换后,两边的值相等即可;

{} == '[object Object]'  // true {}转化字符串就是[object Object]

2)===表示严格相等,即值和类型都要相等,但是NaN不等于自身,以及+0等于-0;
3)Object.is() 完全相等。

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

附:部分类型转换规则如下:
1)null和undefined:最终转换为相同类型;
2)原始数据类型与number类型作比较,先把原始数据类型转化为number再比较;
3)引用数据类型和number类型作比较,各自转化为字符串类型再比较;

四、垃圾回收机制

1. 引用计数

1)当一个变量指向一个引用类型的值,这个引用类型值的引用次数就是1;
2)当另一个变量又这个引用类型的值,这个引用类型值的引用次数加1;
3)当其中的一个变量又指向另一个值了,那么这个引用类型值的引用次数减1;
4)当引用次数变成0时,说明该对象已经不再被需要了;
5)当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。
小结:总的来说,引用计数就是看一个对象是否有指向它的引用, 若没有其他对象指向它了,说明该对象已经不再被需要了,垃圾回收器就会清除其所占内存。

但它会有一个问题:循环引用。即两个对象相互引用,尽管他们已不再使用,这会使得内存不会被回收。

2. 标记清除

主要工作流程:
1)垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记;
2)然后清除环境中的变量以及被环境中的变量引用的变量的标记;
3)剩下的那些仍被标记的会被视为准备删除的变量;
4)垃圾回收器会对其进行内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
小结:总的来说,标记清除工作原理就是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。

采用这种方法后,就不会出现循环引用导致内存泄漏的问题。

五、数组方法

1. 检测方法

let arr = [];
console.log(arr instanceof Array); // true
console.log(Array.isArray(arr));   // true

2. 栈和队列方法

let arr = [];
arr.push(1, 2, 3);    //arr = [1, 2, 3]
arr.pop();            //arr = [1, 2]
arr.unshift(4, 5, 6); //arr = [4, 5, 6, 1, 2]
arr.shift();          //arr = [5, 6, 1, 2]

若直接返回arr.push和arr.unshift,则返回的是长度;
若直接返回arr.pop和arr.shift,则返回的是被删除的那个值;
这些操作方法会影响原数组。

3. 重排序方法

1> reverse 反序排列

let arr = [4, 5, 6, 1, 2];
arr.reverse();   
console.log(arr); // [6, 5, 4, 2, 1]

2> sort 排序

let arr = [4, 5, 6, 1, 2];
arr.sort(function(a, b) {
    return a - b;  //从小到大排列
});
console.log(arr); // [1, 2, 4, 5, 6]
arr.sort(function(a, b) {
    return b - a;  //从大到小排列
});
console.log(arr); // [6, 5, 4, 2, 1]

这些操作方法会影响原数组。

4. 索引与包含方法

let arr = [4, 5, 6, 1, 2];
console.log(arr.indexOf(6)); //存在5,输出索引2
console.log(arr.indexOf(0)); //不存在0,输出索引-1

通常除了用arr.indexOf(val)输出值判断该数组是否存在这个值val外(输出-1就是不存在,不是-1就是存在),
还有arr.includes(val)来判断是否存在。

5. 转换成字符串方法

let arr = [4, 5, 6, 1, 2];
console.log(arr.toString());  //"4, 5, 6, 1, 2"
console.log(arr.join(''));  //"45612"

这些操作方法会影响原数组。

6. 操作方法

1)concat

let arr = [4, 5, 6, 1, 2];
console.log(arr.concat([6, 7]));  // [4, 5, 6, 1, 2, 6, 7]
console.log(arr);  // [4, 5, 6, 1, 2]

这个操作方法不会影响原数组(笔试常考)。

2)splice
a)替换:arr.splice(第几位开始, 删除几个元素, 要放入的值)

let arr = [4, 5, 6, 1, 2];
arr.splice(1, 1, 7) 
console.log(arr); // [4, 7, 6, 1, 2]

b)删除:arr.splice(第几位开始, 删除几个元素)

let arr = [4, 5, 6, 1, 2];
arr.splice(1, 0, 7); // [4, 7, 5, 6, 1, 2]
arr.splice(1, 1);  //删除操作
console.log(arr); // [4, 5, 6, 1, 2]

c)添加:arr.splice(第几位开始,0,要放入的值)

let arr = [4, 5, 6, 1, 2];
arr.splice(1, 0, 7) 
console.log(arr); // [4, 7, 5, 6, 1, 2]

这个操作方法会影响原数组 (笔试常考)。

3)slice 截取
arr.slice(0, 1)截取包括索引0,而不包括索引1的值。

let arr = [4, 5, 6, 1, 2];
console.log(arr.slice(0, 1)); // [4]
console.log(arr);  // [4, 5, 6, 1, 2]

这个操作方法不会影响原数组 (笔试常考)。

7. 遍历(迭代)方法

1)forEach

let arr = [1, 2, 3, 4, 5];
let newArr = []
arr.forEach(function(val) {
        if (val % 2 == 0) {
            newArr.push(val)
        }
    })
console.log(newArr); // [2, 4]

2)some
some与forEach看上去用法相同,但最大的不同是,forEach里面return true后不会停止遍历会继续遍历,而some只要碰到return true就会停止遍历

let arr = [1, 2, 3, 4, 5];
let newArr = []
arr.some(function(val) {
        if (val % 2 == 0) {
            newArr.push(val)
            return true
        }
    })
console.log(newArr); // [2]

3)filter
filter传的参数为回调函数,回调函数传的参数有val和,index和arr;
这个回调函数有一个要求:必须返回一个boolean值;
true:当返回true时,函数内部会自动将这次回调的val加入到新的数组中;
false:当返回false时,函数内部会过滤掉这次val。

最终全部遍历完,这个filter自动生成一个数组,所以需要定义一个变量接收住这个数组。

let arr = [1, 2, 3, 4, 5];
let newArr = arr.filter(function(val) {
    return true
})
console.log(newArr); // [1, 2, 3, 4, 5]

所以可以直接返回的时候给return后面写上选择的条件。

let arr = [1, 2, 3, 4, 5];
let newArr = arr.filter(function(val) {
    return val % 2 == 0
})
console.log(newArr); // [2,  4]

这样比forEach if···更方便简洁。

4)map
自动遍历完后,return什么就是什么。

let newArr2 = arr.map(function(n) {
    return 100
})
console.log(newArr2); // [100, 100, 100, 100, 100]
let newArr2 = arr.map(function(n) {
    return n * 2
})
console.log(newArr2); // [2, 4, 6, 8, 10]

所以如果想对数组所有数做某一个变化则使用map。

8. 归并方法 reduce

reduce作用就是将数组的所有内容进行汇总,比如把数组中所有值加到一起。
arr.reduce(参数1(回调函数), 参数2:带入的初始值(默认是0))

let arr = [1, 2, 3, 4, 5];
let num = arr.reduce(function(previousVal, n) {
    return previousVal + n
}, 0)
console.log(num); // 15

9. 数组扁平化

将数组拉平,不会影响原数组

  • flat
let arr1 = [1, 2, [3, 4]];
let arr = arr1.flat(Infinity); // [1, 2, 3, 4]
  • 递归
let arr1 = [1, 2, [3, 4]];
function fn(arr) {
    let arr1 = [];
    arr.forEach((val) => {
        if (val instanceof Array) {
            arr1 = [...arr1, ...fn(val)];
        } else {
            arr1.push(val);
        }
    })
    return arr1;
}
let arr = fn(arr1); // [1, 2, 3, 4]
  • reduce
function fn(arr) {
    return arr.reduce((prev, cur) => {
        return prev.concat(cur instanceof Array ? fn(cur) : cur)
    }, [])
}
console.log(fn(arr1));

七、字符串方法

1. 根据字符返回位置

1)indexOf(val)
若找到该字符,则返回索引;若找不到则返回-1。
2)lastIndexOf(val)
从后往前找。

2. 根据位置返回字符

1)charAt(i)
根据索引 i 返回字符。

2)str[i]
HTML5, 根据索引 i 返回字符。

3)charCodeAt(i)
根据索引 i 返回字符的 ASCII 码。

3. 字符串操作方法

以下方法都不改变原数组 1)concat(str1,str2,str3...)
用于连接多个字符串。

2)slice(start, end)

  • 包含start索引号,不包含end索引号。
  • 若没有end索引号,就表示从start截取到末尾。

3)substring(start, end) 包含start索引号,不包含end索引号。基本与slice相同,但不接受负值索引号。

4)substr(start,length)
length表示截取的个数。

4. 替换

let str = '112';
let str1 = str.replace('1', '2'); // '212'
let str2 = str.replace(/1/g, '2'); // '222'

5. 字符串转数组

var str = 'red,pink,blue';
console.log(str.split(','));

6. 大小写转换

1) toUpperCase() 转大写
2) toLowerCase() 转小写

八、对象方法

(之后再更)

九、Function中的call,apply和bind

call,apply和bind最强大的地方就是能够扩充函数赖以运行的作用域。 call和apply的用途都是在特定的作用域中调用函数,它们的第一个参数就是其中运行函数的作用域。该参数若是不同的作用域,最后的结果也会不同。

1. call

call()方法,第一个参数是在其中运行函数的作用域this,其余参数都直接传递给函数,传递给函数的参数必须逐个列举出来;

let o = {}

function fn(x, y) {
    console.log(this); //{}
    console.log(x + y); // 3
}
fn.call(o, 1, 2);

call()即key改变this指向,又可以调用函数,常用于继承。

function Father(uname, age, sex) {
    this.uname = uname;
    this.age = age;
    this.sex = sex;
}

function Son(uname, age, sex) {
    Father.call(this, uname, age, sex);
}
let son = new Son('Mary', 18, '女');
console.log(son); //Son {uname: "Mary", age: 18, sex: "女"}

2. apply

apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组(伪数组argument也算)。

let o = {}

function fn(num1, num2) {
    console.log(this); //{}
    console.log(num1 + num2); // 3
}
fn.apply(o, [1, 2]);

我们可以巧妙利用 apply 借助于数学内置对象直接求 数组 的最大值。

let arr = [1, 2, 3];
let max = Math.max.apply(Math, arr);
let min = Math.min.apply(Math, arr);
console.log(max, min); // 3 1

这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。

3. bind

bind()方法适用于不会调用原来的函数,却需要改变原来函数内部的this指向的情况,比如:
需求:当我们点击了之后,就禁用这个按钮,3秒钟之后开启这个按钮

<button>按钮</button>
<script>
    let btn = document.querySelector('button');
    btn.onclick = function() {
        this.disabled = true;
        setTimeout(function() {
            this.disabled = false;
        }.bind(this), 3000)
    }
</script>

原本普通函数this指向window,通过bind(this),则指向了btn。

十、 原型对象相关

1. 原型对象prototype

如果像之前一样,把属性和方法都定义到构造函数本身上,会存在一个缺点:即不同的实例调用该方法时,都要重新创建一遍,十分耗内存,所以这种问题可以通过原型模式来解决。
每一个构造函数都有一个prototype属性,这个prototype属性会指向一个原型对象,我们可以把一些不变的方法定义在原型对象上,这样所有对象的实例都可以共享这些方法,就节省了内存。

2. 对象原型__proto__

实例对象是如何访问到prototype原型对象上的属性或方法?
这是依靠对象实例身上自带的__proto__属性,这个属性指向prototype原型对象,它相当于为对象查找机制提供了一条路(或一个方向)。

3. 原型链

当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的原型对象里找,如果这个原型对象不存在这个属性,就继续往这个原型对象的__proto__属性所指向Object原型对象上找,就这样一级一级往上找,直到找到null。 这样通过__proto__属性将对象链接起来的这条链路称为原型链。

4. 顺便缕清构造函数、prototype原型对象、__proto__属性的关系

十一、new和Object.create的区别

1. 手写new

function Person(name, age) {
    this.name = name;
    this.age = age;
}

function myNew(Func, ...args) {
    let obj = Object.create(Func.prototype);
    let res = Func.apply(obj, args);
    return typeof res === 'object' ? res : obj;
}
let xm = myNew(Person, 'xiaoming', 18);
console.log(xm);
  1. 创建一个空对象,并让其继承Func.prototype;
  2. 将this指向创建的空对象obj,执行构造函数,给这个对象继承属性和方法;
  3. 若构造函数返回结果是对象就直接返回res,否则返回创建的对象obj。

2. 手写Object.create()

function object(o) {
    function F() {};
    F.prototype = o;
    return new F();
}
  1. 创建一个临时性的构造函数
  2. 将传入的对象作为这个构造函数的原型
  3. 最后返回这个临时构造函数的一个新实例

区别:
比较鲜明的区别是在继承的时候

1)使用new进行继承

Child.prototype = new Parent();

从上面的手写js可看出,new继承的同时还调用了父类构造函数(apply),除了继承父类构造函数原型上的属性和方法,这还会将父类构造函数本身的属性和方法继承过来。

2)使用Object.create()进行继承

Child.prototype = Object.create(Parent.prototype)

从上面的手写js可看出,Object.create()继承相当于在Child.prototype和Parent.prototype之间创建了一个中间变量F,这种方法只会继承父类构造函数原型上的属性和方法。

小结: Object.create()它只会继承父类构造函数原型上的属性和方法,比new方法更为纯粹。

十二、 继承

1. 构造函数继承

function Parent1(){
    this.name = "parent1";
    this.colors = ["red","blue","yellow"];
}
function Child1(){
    Parent1.call( this ); // 此处采用构造函数继承
    this.type = "child1";
}
new Child1().name; // Parent1
new Child1().colors; // (3) ["red", "blue", "yellow"]

第6行,在子类(Child1)中执行父类(Parent1)的构造函数,通过这种调用,把父类构造函数的this指向为子类实例化对象引用,从而导致父类执行的时候父类里面的属性都会被挂载到子类的实例上去。

再看定义在原型对象上的~

Parent1.prototype.sex = "男";
Parent1.prototype.say = function() {
    console.log(" Oh,My God! ");
}

new Child1().sex; // undefined
// Uncaught TypeError: (intermediate value).say is not a function
new Child1().say();

缺点: 父类原型上的属性和方法无法继承,而且这种方式也无法实现构造函数的复用。

2. 原型链继承

为了继承父类原型上的属性和方法,可采用原型链继承方法;
这种方法既可以继承父类构造函数里的属性和方法(通过上面书写new的原生js可知,使用new的时候会利用apply调用构造函数);
又可以继承父类构造函数原型上的属性和方法,但这种方法仍有一个缺点

function Parent2(){
    this.name = "parent2";
    this.colors = ["red","blue","yellow"];
}
function Child2(){
    this.name = "child2";
}
Child2.prototype = new Parent2(); // 此处采用原型链方法继承

let s1 = new Child2();
s1.colors.push("black");
let s2 = new Child2();

s1.colors; // (4) ["red", "blue", "yellow", "balck"]
s2.colors; // (4) ["red", "blue", "yellow", "balck"]

缺点: 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改)

3. 组合式继承

为了解决原型链继承方法导致的原型上的属性会共享的缺点,可采用组合式继承。

function Parent3(){
    this.name = "parent3";
    this.colors = ["red","blue","yellow"];
}
function Child3(){
    Parent3.call(this);  // 此处采用构造函数
    this.type = "child3";
}
Child3.prototype = new Parent3();  //此处采用原型链方法
var s1 = new Child3();
s1.colors.push("black");
var s2 = new Child3();

s1.colors; // (4) ["red", "blue", "yellow", "balck"]
s2.colors; // (3) ["red", "blue", "yellow"]

由此可看出问题被解决。
缺点: 父类的构造函数被执行了两次,第一次是Parent3.call(this),这是Child3继承;第二次是Child3.prototype = new Parent3(),这是Child3.prototype继承。

4. 寄生组合继承

采用Child4.prototype = Object.create(Parent4.prototype),可以使Child4.prototype间接指向Parent4.prototype,这样父类构造函数就不用被调用两次了。

function Parent4(){
    this.name = "parent4";
    this.colors = ["red","blue","yellow"];
}
Parent4.prototype.sex = "男";
Parent4.prototype.say = function(){console.log("Oh, My God!")}

function Child4(){
    Parent4.call(this);
    this.type = "child4";
}

Child4.prototype = Object.create(Parent4.prototype);
Child4.prototype.constructor = Child4;

注意这里改变原型对象prototype的指向不能采用Child4.prototype = Parent4.prototype这种方式,因为这样的话父类可能会出现子类的属性或方法。我们需要采用Object.create()创建一个中间对象,不让Child4.prototype直接指向Parent4.prototype

5. ES6中的继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Parent {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    sum() {
        return this.x + this.y;
    }
}
class Child extends Parent {
    constructor(x, y, colors) {
        super(x, y);
        this.colors = colors;
    }
}
let child = new Child(1, 2, 'red');
console.log(child.sum()); // 3

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

参考自 杜尼卜

十三、闭包

1. 闭包的定义

闭包是指有权访问另一个函数作用域中的变量的函数。

2. 闭包产生的原因

要深入了解闭包,就先要了解作用域,因为一个函数之所以能访问另一个函数作用域的变量,就是因为作用域链的存在。

作用域链是指当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去上一级作用域找,直到找到该变量的标示符或者不在其作用域中,这就是作用域链。

闭包产生的本质就是,当前环境中存在指向父级作用域的引用。
当前函数的词法作用域能访问包含它的外层函数的内部作用域,通常这个外层函数执行后,看上去这个函数的内容不会被使用,我们以为会被销毁,但是内部函数仍然保持着对外层函数作用域的引用,所以不会被销毁,外层函数的内部作用域仍然存在,这个就是闭包。

即使外层函数执行完毕,其活动对象不会被销毁,因为内层函数的作用域链仍然引用这个活动对象,

3. 创建闭包的常见形式

  • 在一个函数内部创建另一个函数
function f1() {
  var a = 2
  function f2() {
    console.log(a);//2
  }
  return f2;
}
var f = f1();
f();
  • 在定时器、事件监听等任何异步中,只要使用了回调函数,实际上就是在使用闭包。

    以下的闭包保存的仅仅是window和当前作用域。

// 定时器
setTimeout(function timeHandler(){
  console.log('111');
},100)

// 事件监听
$('#app').click(function(){
  console.log('DOM Listener');
})
  • IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以使用全局的变量。
var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();

4. 闭包的作用

  • 可以模仿块级作用域
  • 可用于在对象中创建私有变量,让闭包实现共有方法,然后通过公有方法访问处在包含作用域中定义的变量

5. 闭包的缺点

由于闭包会携带包含它的函数作用域,所以会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多;而且在一些IE低版本浏览器中,闭包使用不当有可能造成内存泄漏(闭包中一直保持着对变量的引用,该变量占用的内存永远不会被回收。内存泄漏就是由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。 )。

十四、跨域

一)跨域的定义

跨域是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

什么是同源策略?
所谓的同源是指,域名、协议、端口均为相同。

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax请求无法发送

二)跨域解决方案

1. jsonp

由于浏览器允许在html页面中通过相应的标签从不同域名下加载静态资源文件,所以我们可以根据这个原理来进行跨域。

实现方式:
利用script标签里的src属性支持跨域访问,在src中写上请求地址,并在后面拼接一个?callback=func,这样解可以让向服务器发送请求的同时并把本地的一个函数传递给服务器, 然后服务器接收到客户端的请求后,服务器会把func转为字符串的格式,并将准备好的数据拼接到func中,再返回到客户端中,客户端收到后开始执行func,此时客户端就接收到了来自服务器的数据。

1)原生方式

 <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

2)jquery ajax

$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",    
    data: {}
});

3)vue.js

this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

jsonp跨域的缺点
script、img、link、iframe等都是资源文件请求,而资源文件请求都是get请求,所以JSONP的缺点就是只能处理get请求。

2. CORS跨域资源共享

实现方式:

  • 普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置;
  • 若要带cookie请求:前后端都需要设置。

前端设置:

// 原生ajax前端设置是否带cookie
xhr.withCredentials = true;

后端设置:

// 跨域后台设置  部分代码
res.writeHead(200, {
    'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
    'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
    'Access-Control-Expose-Headers': 'Token'
    /* 
     * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
    'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
});

对Access-Control-Expose-Headers的解释:
在CORS中,默认的只允许客户端读取下面六个响应头(在axios响应对象的headers里能看到):

  1. Cache-Control
  2. Content-Language
  3. Content-Type
  4. Expires
  5. Last-Modified
  6. Pragma

如果想让客户端读取到其他响应头,就需要设置Access-Control-Expose-Headers,比如Access-Control-Expose-Headers: Token。

CORS跨域的缺点
只能选择任何源都可以跨域访问或者一个源能跨域访问。

3. Nodejs中间件代理跨域

vue框架下的跨域:

  • 实现方式:
    利用node + webpack + webpack-dev-server代理接口跨域。主要实现方法就是在webpack配置中设置一个代理proxy,并将设置changeOrigin: true
  • 原理:
    在开发环境下,由于vue渲染服务接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true, 
            
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}
4. postMessage跨域

我们常使用postMessage这样方式进行跨页面脚本的数据通信,比如页面与嵌套的iframe进行跨域数据传递。
用法postMessage(data,origin)方法接受两个参数

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1)a.html:(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2)b.html:(www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>
5. WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议,它允许跨域通讯。 原生WebSocket API使用起来不太方便,我们一般使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

前端代码:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    // 发送数据给服务端
    socket.send(this.value);
};
</script>

Nodejs socket后台:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

WebSocket协议跨域与postMessage有点相似,
postMessage是页面A给页面B发送数据,然后页面B接收到数据后进行处理又发送给页面A WebSocket协议是真正的客户端向服务端发送数据,然后服务端接收到数据后进行处理又发送给客户端。
所以这两种跨域方法有点相似,只不过postMessage跨域方法基于的是html5新的API(postMessage),而WebSocket协议跨域基于的是socket.io。

6. document.domain+iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:

两个页面都通过js的document.domain来强制设置相同的基础主域,就实现了同域。

1)父窗口:(www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2)子窗口:(child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>
7. location.hash + iframe 跨域

原理:
a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:
A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

1)a.html:(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2)b.html:(www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3)c.html:(www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>
8. window.name + iframe 跨域

实现原理:
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

1)a.html:(www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2)proxy.html:(www.domain1.com/proxy.... 中间代理页,与a.html同域,内容为空即可。

3)b.html:(www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>
9. nginx代理跨域

(之后再更)

参考自 安静de沉淀

十五、防抖和节流

1. 防抖

事件在频繁被触发的情况下,动作不会被执行,每次被触发都要重新计算时间;只有当事件最后一次被触发后且过了所设置的时间间隔,该动作才会被执行。
防抖操作应用很广泛,比如当我们在搜索框键入文字,它会相应的匹配一些可供选择的文字段,而当你一次性输入10个字符,它就会瞬间发送10个请求,这无疑增加了损耗,所以我们可以采用防抖操作,让它输入完字符再统一发送一次请求。

let myBtn = document.getElementById("btn");
myBtn.addEventListener("click", debounce(sayDebounce));

function debounce(fn) {
    let timeout = null;
    return function() {
        clearTimeout(timeout);
        // 事件触发后的1s内,timeout就有值,在这1s内,紧接着下一次触发就会将timeout清除,函数无法被调用,所以一直不断触发就一直会执行清除操作
        // 不再触发后,1s后就自动调用函数
        timeout = setTimeout(() => {
            fn.call(this, arguments);
        }, 1000);
    }
}

function sayDebounce() {
    console.log('防抖成功');
}

2. 节流

每隔一定的时间,动作只会被执行一次。就算事件一直被触发,动作也只会每隔一段时间执行一次。
节流操作的应用也十分广泛,比如当用户点击一次按钮之后就禁用此按钮,一段时间后才允许点击。

let myBtn = document.getElementById("btn");
myBtn.addEventListener("click", throttle(sayThrottle));

function throttle(fn) {
    let flag = true;

    return function() {
        if (!flag) { // 若为false就不执行后面代码,不会触发事件
            return
        }
        flag = false; // 点击按钮,flag为false,之后就算继续点击按钮也不会再触发,等到1s后设为true才能触发
        setTimeout(() => {
            fn.call(this, arguments);
            flag = true; // 此次事件触发后flag设为true,从而允许下一次触发。
        }, 1000);
    }
}

function sayThrottle() {
    console.log('节流成功');
}