【JS】js学习总结(下)-函数,作用域链,执行上下文,闭包等

目录

一.js内存

二.函数

1.如何定义函数?

2.如何执行调用函数?

3.什么函数叫回调函数?

4.立即执行函数 

5.函数中的this 

6.函数的参数默认值

7.函数原型常用方法 

1)call() / apply()

2)bind()

三.函数的原型和原型链  

1.函数的prototype属性

2.显式原型和隐式原型

3.原型链 

4.原型链深入

1)函数的原型图解

5.基于内置类的原型扩展方法

2)instanceof原理

四.Object的方法 

1.Object.create()

2.Object.prototype.toString()

3.Object.prototype.hasOwnProperty()

4.Object.assign()

5.对象操作的14种方法

五.变量提升和函数提升

六.执行上下文

1.全局执行上下文

2.函数执行上下文

3.执行上下文栈 

七.作用域和作用域链

1.作用域

2.作用域链

八.闭包

1.闭包的基本概念

1)如何产生闭包?

2)什么是闭包?

3)产生闭包的条件?

4)闭包的作用?

5)闭包的生命周期

6)闭包缺点 

2.闭包图解

3.闭包的应用

1)循环事件绑定

2)自定义js模块

九.对象的创建模式 

1.构造函数模式

2.对象字面量模式

3.工厂模式

4.自定义构造函数模式

1)构造函数 + 原型

2)构造函数实例化原理

十. 继承

1.原型链继承

2.借用构造函数继承

3.组合继承

十一.Web Workers

十二.websocket

1.客户端websocket实现

2.服务端socket实现

十三.es6新特性

1.let和const

2.箭头函数及this

3.解构赋值

4.“...”运算符的作用

5.class创建类

1)es6中类的继承

6.模板字符串

7.async/await

8.生成器函数


一.js内存

内存是内存条通电后产生的临时的可存储的数据空间;

内存分为两类,栈内存堆内存;

栈内存存放全局变量局部变量;堆内存存放对象(引用类型的值);

哪些内容放入栈内存?

打开浏览器形成的全局作用域是栈内存;

手动执行函数形成的私有作用域是栈内存;

基于es6中的let/const形成的块作用域也是栈内存

栈内存销毁时机? 

全局栈内存:关掉页面时才会销毁;

私有栈内存:一般情况下,函数只要执行完毕,形成的私有栈内存就会销毁(无限递归,死循环除外)

堆内存销毁时机? 

如果当前创建的堆内存不被其他东西引用了(浏览器会在空闲的时候,查找每一个内存的引用状况,不被占用的都会给回收释放掉)则会释放;

需要手动清除变量和值的关联(赋值为null:空对象指针);

例如:

    let  obj = { a:1 };

    obj = null;

浏览器底层堆栈内存的运行机制

let a = 12;
let b = a;
b = 13;
console.log(a);  // 12


let n = {name:'你好'};
let m = n;
m.name = 'hello';
console.log(n.name); // hello

fbcc4a05d75c4df28b06e7841fe4721b.png

浏览器想要执行上述js代码

1.从电脑内存中分配出一块内存,用来执行代码(栈内存)

2.分配一个主线程用来自上而下执行js代码;

js代码执行过程

1.创建变量a,放到当前栈内存变量存储区域中;

2.创建一个值12,把它存储到当前栈内存值区域中(基本数据类型);

3.=为赋值,其实赋值就是让变量和值相互关联的过程;

4.执行哪行代码代码进栈执行,执行完毕代码立刻出栈;

引用类型值的存储

0.创建变量n,放到当前栈内存变量存储区域中;

1.在内存中分配出一块新内存,用来存储引用类型{name:'你好'}的值(堆内存),该内存有一个16进制的地址;

2.把对象中键值对依次存放到该堆内存中;

3.把堆内存地址和栈内存中创建的变量n关联起来;

基本类型:按值操作,直接操作的是值,所以叫做值类型

引用类型:操作的是堆内存的地址,按照引用地址操作的


二.函数

何为函数?

实现特定功能的n条语句的封装体;只有函数是可以执行的,其它数据类型是不可以执行的

为什么使用函数?

提高代码复用;便于阅读交流

1.如何定义函数?

1)函数声明

function fn1() {
    console.log("fn1");
}
fn1();

2)表达式

var fn2 = function() {
    console.log("fn2");
}
fn2();

注意:

1.函数的返回值只看return,有return后面返回的是啥就是啥,没有return返回的就是undefined

2.创建函数,开辟的堆内存中存储的是函数体中的代码,但是是按照字符串存储的;

3.每一次函数执行的目的都是把函数中的代码先从字符串变成js代码执行,形成一个全新的私有栈内存;

2.如何执行调用函数?

1)直接调用 ---- fn()

2)通过对象调用 ---- obj.fn()

3)new调用 ---- new fn()

4)call/apply ---- test.call(obj)/test.apply(obj)

这种方式可以让一个函数成为指定任意对象的方法进行调用

3.什么函数叫回调函数?

该函数满足:1.你定义的  2.你没有调用  3.最终执行了

常见的回调函数有:dom事件回调函数,定时器回调函数,ajax请求回调函数,生命周期回调函数 

4.立即执行函数 

IIFE,又称匿名函数自调用,它会自动执行,执行完成后立即释放

可以用它来编写js模块,es5不存在块作用域,所以这样写不会污染外部命名空间,隐藏实现

(function() {
    var a = 3;
    console.log(a + 3);  // 6
})();

var a = 4;
console.log(a);  // 4

// ()是执行符号,一定是表达式才能被执行符号执行,还可以这样写
var test = function() {
    console.log(1212);
}();

console.log(test);  // undefined


// 或者 前面加上!~-+

+ function test() {
    console.log(1);  // 1  此时括号前的是一个表达式,函数的名字test会被忽略
}();

在es6之前定义模块,通常使用立即执行函数,可以在匿名函数中定义一个构造函数,随后将这个构造函数保存为window的属性,那么在匿名函数外部就可以直接使用new关键字创建这个构造函数的实例了,这是js插件的开发方法,使用方法如下         

;(function() {
    var a = 1;
    function test(){
        console.log(++a);
    }
    function Person() {

    }
    window.$ = function(){  //  向外暴露一个全局函数
        return {
            test:test
        }
    }
    window.Person = Person;
})();

$().test();  // $是函数,执行后返回对象,调用对象中的test方法

var person = new Person();

5.函数中的this 

什么是this?

任何函数本质上是通过某个对象去调用的,如果没有直接指定的是window;

所有函数内部都有一个变量this;

它的值是调用函数的当前对象。

如何确定this的值?

直接调用函数,如:test(),此时的this是window;

通过对象调用,如:p.test(),此时的this是p;

使用new关键字调用,如:new test() ,此时this是new新创建的对象;

使用call调用,如:p.call(obj),此时this是obj;

6.函数的参数默认值

函数的定义了形参后实参可以不传递,为了避免在函数内部获取实参出现undefined,那么此时可以给函数定义默认参数;

当定义了默认参数后,可以不传实参,相应位置传递undefined;

function test(a=1,b) {
    console.log(a,b); // 1,2
}

test(undefined,2);

但这种传递默认参数的方式对于低版本浏览器不兼容,可以使用函数对象隐含的参数arguments进行判断;

封装实参的对象arguments,是一个类数组对象,可以通过索引调用对象,在调用函数时我们所传递的实参都会在arguments中保存,arguments.length可以用来获取实参的长度;

function test(a,b) {
    var a = typeof(arguments[0]) !== 'undefined' ? arguments[0] :1;
    var b = typeof(arguments[1]) !== 'undefined' ? arguments[2] :2;
    console.log(arguments[0]); // 3
    console.log(b);  // 2
}

console.log(3);

arguments.callee它写在哪个函数就指向当前函数,打印它就是打印当前函数;

function test(a,b,c){
    console.log(arguments.callee.length);  // 3
    console.log(test.length);  //3
    console.log(arguments.length); // 2
}
test(1,2);

callee一般用于匿名函数内部递归调用自身,js严格模式下禁止使用该属性

var sum = (function() {
    if(n <= 1) {
        return 1;
    }
    return n + arguments.callee(n - 1);
})(100);

console.log(sum)  // 5050

7.函数原型常用方法 

1)call() / apply()

这两个方法是函数对象Function原型上的方法,可以通过函数对象来调用,当函数调用call(),apply()时都会调用函数执行

function fun() {
    alert("我是function函数");
}
fun() //等价于fun.call(); fun.apply();

在调用call()和apply()可以将一个对象指定为第一个参数,此时这个对象会成为函数执行时的this 

var obj={name:"obj2"};
fun.apply(obj); //obj

// 相当于对象obj拥有fun方法,随后调用执行

//var obj={
//    name:"obj2"
//    fun:function() {
//        alert("我是function函数");
//    }
//};

//obj.fun();

 call()方法可以将实参在对象之后依次保存;apply()方法需要将实参封装在数组中统一传递

function fun(a,b) {
    lert("我是function函数");
    console.log(a);
    console.log(b);
}

fun.call(obj,2,3);
fun.apply(obj,[2,3]);

2)bind()

bind也是函数对象的方法,同样可以修改被调用函数的this指向,但不同的是bind不会直接执行指定的方法,它会返回指定方法的拷贝,需要手动调用执行,且bind方法传递实参的方式同call,依次传递。

function fun(a,b) {
    lert("我是function函数");
    console.log(a);
    console.log(b);
}

var obj={name:"obj2"};

var funClone = fun.bind(obj,2,5);
funClone();

实际应用:像函数中的arguments这些类数组,它们原型是Object的原型,所以无法调用数组原型上的方法,可以使用call将类数组调用数组原型上的方法

function sum () {
    let arg = Array.prototype.slice.call(arguments,0);
    // 或者可以写成 [].slice.call(arguments,0);
    return eval(arg.join('+'));
}

let total = sum(10,20,30,40);

三.函数的原型和原型链  

js面向对象:是一种编程思想,将js中的事物按照功能特点分成类,我们研究的是类的实例,如果某一个实例具有某些特征特性,那么当前类的其他实例也具有某些特征特性;js拥有许多内置类,我们也可以自己创建类;

面向对象的特性:

封装:低耦合高内聚;

多态:重载和重写

重载:方法名相同,形参个数或者类型不一样,js中不存在真正意义上的重载,js中重载指的是同一个方法,根据传参个数,实现出不同的效果(需要在方法内部对参数进行判断)

重写:在类的继承中,子类可以重写父类中的方法;

继承

创建类的实例就是将普通函数使用new的方式执行,js中的类和实例是基于原型和原型链机制来处理的;

1.函数的prototype属性

每一个函数都有prototype属性,它默认指向一个Object空对象,这个对象称为原型对象;

function Fun() {}
console.log(Fun.prorotype);

 这个就是Fun.prototype默认指向的Object空对象:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

原型对象中有一个属性constructor,它指向函数对象;所以说函数和它的原型对象之间存在相互引用;

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

给原型对象添加属性或方法,函数的所有实例对象自动拥有原型中的属性(方法)

2.显式原型和隐式原型

每个函数function都有一个prototype,即显式原型属性;每个实例对象都有一个__proto__,即隐式原型属性;对象的隐式原型的值为其对应的构造函数的显式原型的值;

function Fn() {}

console.log(Fn.prototype);  // 函数有显示原型属性

var fn = new Fn();  // 内部:this.__proto__ = Fn.prototype

console.log(fn.__proto__);  // 实例存在隐式原型属性
 
console.log(Fn.prototype === fn.__proto__);  // 显示原型等于隐式原型

函数对象的原型图解 

创建函数fn,就在栈内存中创建了存储单元指向堆内存中的function对象0x123,对象内存在原型属性prototype,它指向函数的原型0x234;

随后创建函数的实例,栈内存使用0x345引用,在实例对象中,存在隐式原型属性__proto__,它也指向函数的原型对象0x234;

在Fn中添加方法test,实际上就是给原型对象0x234添加方法test;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

隐式原型属性__proto__可以修改,但不能自造,自造的隐式原型没有实际意义。

3.原型链 

函数fn的prototype指向的函数原型对象是Object的实例化对象,由于是实例化对象所以它也具有一个隐式原型__proto__属性;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

所以在我们的函数定义之前就存在Object对象,他里面有一个prototype属性,并且其地址值与之前函数fn原型对象的隐式原型__proto__的地址值是一致的,都指向Object的原型对象;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

原型链图解

函数自身声明的属性和方法都存放在函数的实例对象中,如test1(),调用方法test1(),直接在fn实例对象上查找;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 调用方法test2(),它是函数原型上的方法,在函数fn的原型上查找不到,就通过函数实例的__proto__属性去函数的原型对象上查找;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

调用方法fn.toString(),它是Object原型上的方法,先在在函数fn的原型上查找不到,就通过函数实例的__proto__属性去函数的原型对象上查找,依旧找不到,再通过fn原型的__proto__属性,在Object的原型上查找;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

调用方法fn.test3(),按照上述方法顺着原型链查找,最终找到Object的原型对象,无法找到,所以查找Object原型对象的__proto__属性,这个值为null,代表的是原型链的尽头,说明不存在test3方法,返回undefined,但执行了test3(),相当于调用了undefined,所以报错。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

原型链总结:

作用:查找对象的属性/方法(查找变量是通过作用域链)

查找过程:

1)访问一个对象的属性时,先在自身身上查找,找到返回;

2)如果没有,沿着__proto__这条链向上查找,找到返回;

3)一直找到Object.prototype为止,最终没有找到,返回undefined

4.原型链深入

1)函数的原型图解

之前一直探索的是构造函数的原型,但要知道,函数对象也是实例对象,它是Function的实例,那么它也拥有隐式原型属性__proto__,它指向Function构造函数的(显式)原型。

两者是等价的:function fn() { ...... }      <=>    var fn = new Function();

Function类的原型是一个匿名函数(其他所i有类的原型都是对象),其中有如call(),apply(),bind()  toString()等方法;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

对于Function构造函数,它也存在隐式原型属性__proto__,但值得注意的是,它自身的隐式原型属性指向它的显式原型,即Function.__proto__ = Function.prototype;

因为实例对象的隐式原型等于构造函数的显示原型,从而说明Function构造函数是自己创建的,即:

Function = new Function()

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_13,color_FFFFFF,t_70,g_se,x_16

此外,Object对象是js的内建对象,它的创建也是需要经过var Object = new Function() 产生,所以说Object是Function构造函数的实例对象,所有函数对象的隐式原型都指向Function的显示原型。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_13,color_FFFFFF,t_70,g_se,x_16

Object.__proto__.__proto__ === Object.prototype;  // true

5.基于内置类的原型扩展方法

在内置类原型上的方法,类对应的实例可以直接调用,例如:实例.方法();如果我们也可以把自己写的方法放到原型上,那么当前类的实例也可以直接这样调用。

需要注意的是:自己扩展的方法不能影响原有内置类的方法(最好在方法名前加前缀my)

示例:在数组原型上添加一个方法,用于数组去重

function myUnique () {
    let obj = {};
    for(let i = 0;i < this.length; i++) {
        let item = this[i];
        if(typeof obj[item] !== '') {
            this[i] = this[length - 1];
            this.length--;
            i--;
            continue;
        }
        obj[item] = item;
    }

    obj = null;
    return this;
}

Array.prototype.myUnique = myUnique;

let array = [122,34,67,454,67,899,122,908];
array.myUnique().sort((a,b) => { return a - b; });

1.myUnique方法就是我们自定义的原型扩展方法;

2.该方法没有传递要操作的数组进来,但是方法中的this就是当前要操作的数组

3.保证当前方法执行完返回的结果依旧是array类的一个实例,从而可以实现方法的链式调用

总结:

1.函数的显式原型属性默认指向空的Object实例对象,但Object的显式原型不满足;

function Fn() {}
console.log(Fn.prototype instanceof Object);  // true
console.log(Object.prototype instanceof Object);  // false
console.log(Function.prototype instanceof Object);  // true

2.所有函数都是Function的实例,包括Function本身

console.log(Function.__proto__ === Function.prototype); // true

3.Object的原型对象是原型链的尽头

console.log(Object.prototype.__proto__);  // null

4.读取对象属性时,会自动到原型链中查找;设置对象的属性时,不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值

function Fn() {}

Fn.prototype.a = 'aaa';
var fn1 = new Fn();

var fn2 = new Fn();
fn2.a = 'bbb';
console.log(fn1.a,fn2.a);  //  aaa  bbb

5.方法一般定义在原型中,属性一般通过构造函数定义在对象本身上


2)instanceof原理

表达式:A instanceof B (A是实例,B是构造函数)

如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false

示例1:

function Foo() {......}
var f1 = new Foo();
console.log(f1 instanceof Object);  // true

f1为构造函数Foo的实例,原型链图解如下,此时Object的显式原型对象在f1的隐式原型链上,返回true; 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 示例2:

console.log(Function instanceof Object);  // true
console.log(Object instanceof Object);  // true
console.log(Function instanceof Function);  // true
console.log(Object instanceof Function);  // true

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_18,color_FFFFFF,t_70,g_se,x_16

局限性:

1.要求检测的数据类型必须是对象数据类型,基本数据类型的实例是无法基于它检测出来的

console.log(1 instanceof Number);  // false

四.Object的方法 

1.Object.create()

用于创建对象,但和new关键字不同的是,可以为添加的这个对象自定义原型,不需要系统自己创建。参数为手动指定的所创对象的原型或null,Object.create([prototype][,ownpropertiesObject])

function Obj() {}
Obj.prototype.num = 1;
var obj1 = Object.create(Obj.prototype);  // 创建了一个新对象,其原型是Obj的原型
var obj2 = new Obj(); // 这种方法和上面方法效果一样

// 创建一个对象
var test = {num:2};
// 创建一个新对象,它的原型是test对象
var obj3 = Object.create(test);

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 如果参数传递的是null,那创建的就是一个纯粹的空对象;这个对象不继承Object.prototype,所以该方法也可以实现原型链继承;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16


该方法存在第二个参数,用来指定创建的对象的私有属性,例如:

let obj = Object.create({a:1},{
    b:{
        // descriptor
        value:2,
        enumerable:true, // 是否可for循环枚举,默认为false
        configurable:true, // 是否可删除,默认为false
        writable:true // 是否可修改,默认为false
    },
    c:{
        value:4,
        enumerable:true
    }
});

console.log(obj);

添加的私有属性必须是属性描述器对象的形式,在该对象中以属性描述符的方式配置对应的私有属性,常用的属性描述符有enumerable,configurable,writable,value,其具体含义间上述代码段。  

96abaff13b7f44a5a0eef53a84847edd.png

2.Object.prototype.toString()

当我们直接在页面上打印一个对象时,实际上是打印这个对象的toString()方法的返回值,[object,Object]

所以说该方法可以用来检查数据类型,但需要注意,不同类型的子类继承Object时重写了toString方法,所以判断是需要使用call/apply

new Object().toString();  // '[object Object]'
Object.prototype.toString.call(new Date); // '[object Date]'
Object.prototype.toString.call(new String); // '[object String]'
Object.prototype.toString.call(Math); // '[object Math]'
Object.prototype.toString.call(new Array());  // '[object Array]'
Object.prototype.toString.call(1);  // '[object Number]'

......
//Since JavaScript 1.8.5
Object.prototype.toString.call(undefined); // '[object Undefined]'
Object.prototype.toString.call(null); // '[object Null]'

 包装类Number,String,Boolean,以及数组Array,会重写toString方法,会返回对应类型转化怒成string类型的字符串结果。

[1,2].toString();   // '1,2'
Number.prototype.toString.call(1); // '1'
......

null和undefined无法使用toString() 

3.Object.prototype.hasOwnProperty()

可以使用对象的hasOwnProperty(“属性名”)方法来检查某个属性是否为当前对象的私有属性,返回布尔值,对于对象原型上的属性无法进行判断。而for...in...遍历对象时会将对象原型上的属性一同遍历出来,所以可以搭配hasOwnProperty()过滤掉原型上的属性。

4.Object.assign()

用于对象合并,语法:Object.assign([目标对象],[源对象1][,源对象2],......)

let target = {a:1,b:2};
let source1 = {b:4,c:6};
let source2 = {b:8,e:9};

let returnTarget = Object.assign(target,source1,source2);
console.log(returnTarget);  // {a: 1, b: 8, c: 6, e: 9}

该方法最终会返回目标对象target,其对象合并分配的原理是使用数据劫持,调用source对象的get方法,再通过target对象的set方法添加到目标对象中。这里的getter和setter是js中对象的访问描述符。

注意:

1. 该方法合并对象时不会合并源对象原型上的属性;

2. 源对象中可枚举的对象属性enumerable:true才会分配到目标对象中去;

3. 属性分配不会将该属性的描述符一起分配,如a属性原来设置不可删除configurable:false,对象合并后的a属性是可以删除的;

4. 若源对象不是对象类型,则会通过包装类包装,再将可枚举的对象合并到目标对象中;

示例1 

const v1 = 123;
const v2 = '123';
const v3 = true;
const v4 = function test () {};

const v5 = Object.assign({},v1,v2,v3,v4);
console.log(v5); // {0: '1', 1: '2', 2: '3'}

其中v1,v3,v4通过对应类型的包装类包装后的结果都是不可枚举的,只有v2字符串类型通过new String(v2),返回的结果String {'123'} 0: "1" 1: "2" 2: "3" 是可枚举的,最终间将其合并到目标对象中返回。

示例2

const tar1 = Object.defineProperty({},'a',{
    value:1,
    writable:true,
});

const res = Object.assign(tar1,{b:2},{b:3,a:4},{c:9});

for(let k in tar1) { console.log(k,tar1[k])}  // b 3 c 9
for(let j in res) { console.log(j,res[j])} // b 3 c 9

由于目标对象tar1的a属性是不可枚举的,而合并后的对象是目标对象的引用,所以结果中a也是不可枚举的;

示例3

自定义assign获取对象中get/set修饰的方法。使用Object.assign无法合并对象中get/set修饰的方法,需要自定义实现;

对象中的get和set关键字的作用

get 关键字将对象属性与函数进行绑定,当属性被访问时,对应函数被执行。
set 关键字将对象属性与函数进行绑定,当属性被赋值时,对应函数被执行。

使用getOwnPropertyDescriptors(obj,key)获取指定对象中指定属性的信息,最终获取到的结果如下所示: 

b654ef57f7ba4fc191f763cabd16eae1.png 

使用defineProperties(obj,propertiesObject)可以为对象obj一次添加多个属性,这里将上述打印出的descriptors对象中的属性批量添加给target

let source = {
    a:1,
    get b () {
        return 89;
    }
};

Object.myAssign = function (target,...source) {
    source.forEach(source => {
        const descriptors = Object.keys(source).reduce((descriptors,key) => {
            descriptors[key] = Object.getOwnPropertyDescriptor(source,key);
            return descriptors;
        },{});
        console.log(descriptors)
        // 给对象批量添加属性
        Object.defineProperties(target,descriptors);
    });
    return target;
}
console.log(Object.myAssign({},source));

 最终合并的对象如下所示:

5940e073f9174cef9400c6634d55e768.png

5.对象操作的14种方法

前端架构建议多使用函数式方法操作,函数式方法相较于关键字(如instanceOf,prototype),扩展性更强,方便维护。

1)获取原型GetPrototypeOf()

使用js内建的方法[[GetPrototypeOf]]获取原型对象,我们可以使用GetPrototypeOf()方法获取对象原型,当然以下方法都可以获取原型

使用底层提供的api获取

var obj = {a:1,b:2};
console.log(Object.getPrototypeOf(obj))

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

或直接访问对象实例的隐式原型__proto__

console.log(obj.__proto__)

 或访问对象本身的原型容器获取

console.log(Object.prototype);

2)设置原型setProrotypeOf()

底层使用[[setProrotypeOf]]给一个对象设置原型,我们可以使用setProrotypeOf()方法设置

var obj = {a:1,b:2};
Object.setPrototypeOf(obj,{a:1,b:2});
console.log(obj)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_18,color_FFFFFF,t_70,g_se,x_16

3)获取对象的可扩展性isExtensible()

底层使用[[IsExtensible]]获取,我们可以使用IsExtensible()方法获取,结果是一个布尔值,一个对象的可扩展性是指这个对象是否可以删除,增加 ,枚举等操作。

一个对象默认可扩展性是true

var obj = {a:1,b:2};
var extensible = Object.isExtensible(obj);
console.log(extensible)

使用Object.freeze()冻结对象,冻结后的对象可扩展性为false

Object.freeze(obj);
var extensible = Object.isExtensible(obj);
console.log(extensible); // false

 扩展:Object.seal()可以封闭一个对象,一个封闭的对象可写,可读(枚举),但不可添加不可删除

var obj = {a:1,b:2};
Object.seal(obj);
obj.c = 3;
console.log(obj); // {a: 1, b: 2} 不可添加

delete obj.a;
console.log(obj); // {a: 1, b: 2} 不可删除

obj.b = 4;
console.log(obj); // {a: 1, b: 4} 可修改

for(var key in obj) {  // 可以遍历原型上的属性
    console.log(obj[key]);  // 1 4
}

Object.freeze()冻结对象,一个冻结的对象,不可写,可读(枚举),不可删除,不可添加

var obj = {a:1,b:2};
Object.freeze(obj);
obj.c = 3;
console.log(obj); // {a: 1, b: 2} 不可添加

delete obj.a;
console.log(obj); // {a: 1, b: 2} 不可删除

obj.b = 4;
console.log(obj); // {a: 1, b: 3} 不可修改

4)获取自有属性getOwnPropertyNames()

js底层使用[[GetOwnProperty]]获取,我们可以使用Object.getOwnPropertyNames()获取非原型属性。

var obj1 = {a:1,b:2};
Object.setPrototypeOf(obj1,{c:2,d:5});
console.log(Object.getOwnPropertyNames(obj1)); // ['a', 'b']

Object.getOwnPropertyDescriptors()获取自有属性的信息

var obj1 = {a:1,b:2};
Object.setPrototypeOf(obj1,{c:2,d:5});
console.log(Object.getOwnPropertyDescriptors(obj1));

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

5)禁止扩展对象preventExtensions()

js底层使用[[PreventExtensions]]进行,使用Object.preventExtensions()操作,一个对象禁止扩展后,可以删除但不可增加属性

var obj1 = {a:1,b:2};
Object.preventExtensions(obj1);
obj1.c = 12;
console.log(obj1);  // {a: 1, b: 2}

6)拦截对象defineProperty()

js底层使用[[defineOwnProperty]]进行拦截,我们使用Object.defineProperty()进行拦截操作,其具体使用如下:

Object.defineProperty(对象,'设置什么属性',{
    writeable,  //设置属性可写
    configable, //设置属性可配置(删除)
    enumerable, //控制属性是否可枚举,是否可被for...in循环取出
    set(){},  //赋值触发
    get(){}   //取值触发
});

示例: 

let o = {};
let _gender;
Object.defineProperty(o,'gender',{
    configurable:true,
    enumerable:true,
    get(){
        //使用o.gender来访问数据,就会调用get方法
        return _gender;
    },
    set(newVal){
        //如果使用o.gender = 'xxx' 那么就会调用set方法,
        _gender = newVal;
    }
});

上述代码实现给o对象添加一个gender属性,并设置该属性为可配置可枚举,并给其添加getter和setter,当访问该属性时会触发get()方法,当修改该属性时会触发set()方法,从而可以在访问和修改该属性时在这两个方法中执行一些额外的操作,从而实现了对象的拦截操作。

当然给一个对象添加属性可以直接使用对象名.属性名的方式,但是这种方式就无法像上面那样为属性添加属性描述符用来约束,更无法实现数据劫持。

7)判断是否是自身属性hasOwnProperty()

js底层使用[[hasProperty]],使用Object.hasOwnProperty()判断

var obj1 = {a:1,b:2};
obj1.hasOwnProperty('a'); // true

8)获取属性 对象名.属性名

在一个对象中访问一个属性,以及判断一个属性是否在对象中js底层都使用内建方法[[GET]]进行

var obj1 = {a:1,b:2};
console.log('a' in obj1);  // true
console.log(obj.a); // 1

9)设置属性 对象名.属性名=属性值

修改对象的属性使用内建方法[[SET]进行

var obj1 = {a:1,b:2};
obj1.a = 3;
obj['b'] = 4;
console.log(obj);  {a:3,b:4}

10)删除属性 delete 对象名.属性名

删除对象的属性js底层使用内建方法[[DELETE]进行

var obj1 = {a:1,b:2};
delete obj1.a;
console.log(obj1); // {b:2}

11)枚举对象 for in

枚举对象[[Enumerate]]

var obj1 = {a:1,b:2};
for(var k in obj) {
    console.log(obj[k]);  // 1 2
}

12)获取键集合 keys()

[[OwnPropertykeys]],使用Object.keys(),获取对象属性,返回一个数组,无法获取原型上的属性

var obj1 = {a:1,b:2};
console.log(Object.keys(obj1));  // ['a', 'b']

13)调用函数

function test() {} 
test();

obj.test = function() {}
obj.test();

14)实例化对象 

function Test() {}
new Test();

五.变量提升和函数提升

当浏览器开辟出供代码执行的栈内存后,代码并没有自上而下立即执行,而是继续做一些事情,把当前作用域中var/function关键字的变量进行提前声明和定义,这就是变量提升机制

1.何为变量声明提升?

通过var定义的变量,在定义语句之前就可以访问到,值为undefined。

2.何为函数声明提升?

通过function声明的函数,在声明之前就可以直接调用,值为函数定义。如下列fn2,代码执行前,浏览器会将fn2作为变量存储在占内存中,遇到引用类型,在堆内存中开辟一个新的内存,把函数的函数体以字符串的形式存储在堆内存中,随后将变量fn2和此堆内存的地址关联在一起。在调用函数fn2时,此时函数已经存在了,故可以执行。

console.log(b);  // undefined
fn2();  // fn2

var b = 1;
function fn2() {
    console.log('fn2');
}

函数提升必须使用声明的方式,使用函数表达式的方式,由于使用VAR变量来创建函数如下fn3,变量提升阶段只会声明变量,不会赋值,所以此时函数在定义语句前面执行,函数是没有值的,不能成功执行,项目中这种方式最常用,因为它操作严谨。

fn2();
fn3();  // 这属于变量提升,不会调用

function fn2() {
    console.log('fn2');
}

var fn3 = function() {
    console.log('fn3');
}

注意1:let和var的区别

1.let/const/import/class声明的变量不存在变量提升,var声明的变量存在变量提升;

2.如果使用var/function关键词声明的变量允许重复声明使用let/const...声明的变量,浏览器会校验当前作用域中是否存在这个变量,若存在,再次声明就会报错;

3.基于let/const/class等创建的变量,会把所在的大括号(除对象的的大括号之外)当作一个全新的私有块级作用域。

注意2:变量带var和不带var的区别

在任何作用域下,声明变量不带var相当于给全局window设置了一个属性,在全局作用域下声明变量带var同样相当于给window增加了一个属性,但在私有作用域下添加为执行上下文的属性。

注意3:代码的词法检测

在浏览器开辟栈内存供代码自上而下执行前,不仅有变量提升的操作, 还有很多其他的操作,例如词法解析,就是检测当前即将要执行的代码是否会出现语法错误(SyntaxError),如果出现错误,所有代码都不会执行;

下面这段代码由于let变量重复声明的语法错误,所有输出都不会执行

// Uncaught SyntaxError:Identifier 'a' has already been declared
console.log(1); 
let a = 12;
console.log(a);
let a = 13;
console.log(a);

注意4:在条件语句中的变量提升

在老版本的浏览器中,无论判断语句的条件是否成立,语句体中的函数会提前声明定义,在新版本浏览器中,为了兼容es6语法规范,条件中的函数在变量提升阶段只能提前声明,不能提前定义,只有当条件成立时再去其中的变量。

fn();  // fn is not a function 
if('fn' in window) {
    function fn () {
        console.log('HHH');
    }
}

注意5:变量声明时的写法

var a = 10,
    b = 20;
// 等价于
var a = 10;
var b = 20;

var a = b = 10;
// 等价于
var a = 10;
b = 10;

六.执行上下文

1.全局执行上下文

1)在执行全局代码执行前将window确定为全局执行上下文GO;

2)接下去对全局数据进行处理,其中:

  1. var定义的全局变量为undefined,添加为window的属性;
  2. function声明的全局函数,添加为window的方法;
  3. this赋值为window;

3)开始执行全局代码

示例1:

var a = 1;
function a() {
    console.log(2);
}

console.log(a); // 1
GO = {
    a:undefined -> function a() {} -> 1
}

2.函数执行上下文

在执行调用函数时产生,函数执行完毕后销毁

1)在调用函数,准备执行函数体之前,创建对应的函数执行上下文AO(虚拟的,在栈内存中)

2)对局部数据进行处理

  1. 形参变量赋值实参,添加为函数执行上下文的属性;
  2. var定义的局部变量添加为执行上下文的属性;
  3. arguments赋值实参列表,添加为函数执行上下文的属性;
  4. function声明的函数,添加为函数执行上下文的方法;
  5. this赋值为调用函数的对象

3)开始执行函数体代码;

示例1:判断下列输出结果

function test(a) {
    console.log(a); // f a() {}
    var a = 1;
    console.log(a); // 1
    function a() {}
    console.log(a); // 1
    var b = funcion(){}
    console.log(b);
}

test(2);
AO = {
    a:undefined -> 2 -> function a() {} -> 1,
    b:function() {}
}

示例2:判断下列输出结果

function test() {
    var a = b = 1;
    console.log(b); // b
}

 在函数内部不用var声明的变量会添加到全局执行上下文中

GO = {    //  全局执行上下文
    b:1
},
AO = {    // 函数执行上下文
    a:undefined
}

3.执行上下文栈 

var a = 10;
var bar = function(x) {
    var b = 5;
    foo(x + 5);
}

var foo = function(y) {
    var c = 5;
    console.log(a + c + y);
}
bar(10);

以上代码会出现三个执行上下文,当出现多个执行上下文,使用栈结构进行管理;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

1)在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文;

2)在全局执行上下文确定后,将其添加入栈中;

3)在函数执行上下文创建后,将其添加到栈中;

4)在当前函数执行完后,将栈顶的对象移出;

5)当所有代码执行完后,栈中只剩下window

七.作用域和作用域链

1.作用域

作用域相当于一块地盘,一个代码所在的区域,它是静态的,在编写代码的时候就确定的;

作用:隔离变量,不同的作用域下同名的变量不会冲突

// 全局作用域开始
var a = 10,
    b = 20;

// fn作用域开始
function fn(x) {
    var a = 100,
        c = 300;
    console.log('fn',a,b,c,x); // 100,20,300,10

    // bar作用域开始
    function bar(x){
        var a = 1000;
            d = 400;
        console.log('bar',a,b,c,d,x); // 1000,20,300,400,100/200
    }
    // bar作用域结束
    
    bar(100);
    bar(200);
}
// fn作用域结束

fn(10);
// 全局作用域结束

分类:

1)全局作用域

2)函数作用域:函数每执行一次就会形成一个全新的私有作用域;

区别作用域和执行上下文:

1)全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时;全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建;函数执行上下文环境是在函数调用时,函数体代码执行之前创建

2)作用域是静态的,只要函数定义好就一直存在,不会变化;执行上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会被释放;

3)上下文环境是从属于所在的作用域,全局执行上下文环境从属于全局作用域;函数执行上下文环境从属于函数作用域;

2.作用域链

函数也是一种对象类型,不如可以访问一个函数的name或者length属性等,如下

function fn(a,b) {}

console.log(fn.name,fn.length); // fn 2

而在函数对象中有一些属性是我们无法访问的,它们是JS引擎固有的隐式属性。隐式属性[[scope]]是函数创建时JS内部生成的;它是存储函数作用域链的容器,在作用域链中存储AO和GO;

AO:函数的执行期上下文;GO:全局执行期上下文;

在函数执行完毕后,AO会销毁,每一次执行函数会生成一个新的执行期上下文;

作用域链就是将这些AO/GO形成链式从上到下排列,通过一个例子进一步解释:

function a() {
    function b() {
        var b = 2;
    }
    var a = 1;
    b();
}
var c = 3;
a();

当a函数被定义时,系统生成[[scope]]属性,[[scope]]保存该函数的作用域链,该函数的作用域链第0位存储当前环境下的全局上下文GO,GO里存储全局下的所有对象,包括函数a和全局变量c;

值得注意的是,每一个函数的作用域链中都是包含全局执行上下文,如下图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

当函数a被执行的前一刻,作用域链的顶端存储a函数生成的函数执行期上下文,同时第一位存储GO;

所以查找变量是从a函数作用域中开始,再到全局中查找,即从顶端开始依次向下找;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

在外层a函数执行时,b函数被定义,是在a函数环境下的,所以此时b函数的作用域链就是a函数被执行期间的作用域链;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

当函数b被执行的前一刻,生成函数b的[[scope]],存储函数b的作用域链,顶端的第0位存储b函数的AO,a函数的AO和全局上下文依次向下排列;

由于在函数a的作用域链中不存在函数b的AO,这也是为什么外部作用域无法访问内部作用域变量的原因;

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

b函数被执行完毕后,b函数的AO被销毁,回归被定义时的状态;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

a函数被执行结束时,a函数的AO被销毁的同时,b函数的[[scope]]也将不存在,a函数回归被定义时的状态;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

作用域链查找机制:

私有栈内存中代码执行的时候,如果遇到一个变量,首先你看是否为当前作用域的,是当前作用域的就是用当前的,不是则去上级作用域中查找,一直找到全局作用域为止。找到拿来用找不到则报错。

确定上级作用域:

函数的上级作用域和函数在哪里执行没有关系,只和函数在哪里创建有关,在哪里创建函数的上级作用域就是谁。

八.闭包

1.闭包的基本概念

1)如何产生闭包?

当一个嵌套的内部子函数引用嵌套的外部父函数的变量/函数的时候,就产生了闭包;

如下,内层函数fn2引用了外层函数fn1的变量a,从而产生了闭包

function fn1() {
    var a = 2;
    var b = 'abc';
    function fn2() {
        console.log(a);
    }
}
fn1();

在浏览器开发者工具中:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

2)什么是闭包?

闭包是嵌套的内部函数;闭包是包含被引用变量/函数的对象(上述案例中的a)

3)产生闭包的条件?

1.函数嵌套;

2.内部函数引用了外部函数的数据,可以是变量也可以是函数;

3.外部函数必须调用;

4)闭包的作用?

1.使用函数内部的变量在函数执行完毕后,任然存活在内存中,延长了局部变量的生命周期

2.让函数外部可以操作到函数内部的数据  保护

下面这段代码中,由于变量f引用了函数fn1的返回值,即函数fn2,而fn2中保存着变量a;

function fn1() {
    var a = 2;
    function fn2() {
        a ++;
        var b = 2;
        console.log(a);
    }
    return fn2;
}

var c = 3;
var f = fn1();
f();  // 3
f();  // 4

5)闭包的生命周期

产生:在嵌套的内部函数定义执行完成就产生了,不是在调用的时候;

死亡:在嵌套的内部函数成为垃圾对象时;

以上述例子为例:

function fn1() {
    // 此时闭包已经产生了
    var a = 2;
    function fn2() {
        a ++;
        console.log(a);
    }
    return fn2;
}

var f = fn1();

// 闭包死亡,包含闭包的函数对象成为垃圾对象
f = null;

6)闭包缺点 

函数执行完后,函数内部的局部变量没有释放,占用内存时间过长,容易造成内存泄漏;闭包使用完后要及时释放;

何为内存溢出?

是一种程序运行出现的错误,当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误;以下代码就会引发内存溢出:

var obj = {};
for(var i = 0; i < 10000; i++) {
    obj[i] = new Array(1000000000);
}

何为内存泄漏?

占用的内存没有及时释放,内存泄漏积累多了就容易导致内存溢出,常见的内存泄露有:意外的全局变量,没有及时清理计时器或回调函数,闭包。

// 意外的全局变量
function fn() {
    a = 3;  // 这里的a在函数调用完毕后必会被销毁,因为没有使用var声明,从而会被当作全局变量处理 
    ......
}

fn();

// 没有及时清理计时器或回调函数
var id = setInterval(function() {
    console.log('aaa')
},1000);



2.闭包图解

有以下代码,产生闭包,通过图解形式分析闭包原理,加深理解

function fn1() {
    var a = 2;
    function fn2() {
        a ++;
        var b = 2;
        console.log(a);
    }
    return fn2;
}

var c = 3;
var f = fn1();
f();  // 3

在fn1函数被定义时,系统生成[[scope]]属性,[[scope]]保存的是该函数的作用域链,该作用域链的第0位存储的是当前环境下的全局执行上下文GO,GO内部存储全局下的所有对象,其中包括函数fn1和全局变量c;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 当fn1函数被执行的前一刻,函数fn2被定义,此时fn2的作用域链和fn1的完全相同

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

当fn1函数执行结束时,因为fn2被返回到外部,且被全局变量接收,此时fn1的AO并没有销毁,只不过fn1的作用域链不再引用了,但此时fn2的作用域链依旧引用着;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

f函数执行时,fn2的作用域链增加自己的AO,当打印a的时候,在自己的AO上没有查找到,则向fn1的AO查找。再次执行f函数时,实际操作的仍然是原来fn1的AO;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 当f函数执行完毕后,fn2的AO被销毁,但原来的fn1的AO仍然存在且被fn2引用;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

3.闭包的应用

1)循环事件绑定

采用以下方法点击弹出的始终为’第4个‘,这是因为当for循环执行完毕后再触发事件回调的。我们需要在每一次循环时将当前的按钮是第几个保存起来,在触犯单击事件回调时获取存放的按钮编号,这里就需要使用闭包保存

var btn = document.getElementByTagName('button');

for(var i = 0,length = btn.length; i < length; i++) {
    var btn = btns[i];
    btn.onclick = function() {
        alert('第' + (i + 1) + '个');
    }
}

使用匿名函数即调用的方式,实际上就是使用了闭包,内层函数是单击事件的回调,它引用外层函数即匿名函数作用域中的i ,而每一个按钮的单击事件onclick始终引用这内层回调函数,导致其不被垃圾回收。

var btn = document.getElementByTagName('button');

for(var i = 0,length = btn.length; i < length; i++) {
    (function (i) {
        var btn = btns[i];
        btn.onclick = function() {
            alert('第' + (i + 1) + '个');
        }
    })(i)
}

2)自定义js模块

定义一个具有特定功能的js模块,将所有的数据和功能是私有的封装在一个函数内部;只向外暴露一个包含n个方法的对象或函数;使用者通过模块暴露的对象调用方法来实现对应的功能;

doSomething函数中引用了外部匿名函数的变量msg,产生闭包;

myModule.js:

(function (w) {
    var msg = 'My modules';
    function doSomething() {
        console.log('doSomething' + msg);
    }
    function doOtherthing() {
        console.log('doOtherthing' + msg);
    }
    
    w.myModule = {
        doSomething:doSomething,
        doOtherthing:doOtherthing
    }
})(window)

main.js:

<script src="./myModule.js"></script>
<script>
    myModule.doSomething(); // doSomething
</script>

九.对象的创建模式 

1.构造函数模式

先创建Object空对象,再动态添加属性和方法;适用于起始不确定对象内部数据的场合。

var p = new Object();
p.name = 'tom';
p.age = 12;
p.setName = function(name) {
    this.name = name;
}

p.setName('JACK');

2.对象字面量模式

使用{}创建对象,同时指定属性和方法;使用于起始时对象内部数据是确定的;

var p = {
    name:"tom",
    age:12,
    setName:function(name){
        this.name = name;
    }
};

p.setName("Jack");

3.工厂模式

通过工厂函数动态创建对象并返回;适用于创建多个对象的场合;创建的对象没有具体的类型,都是Object类型;

function createPerson(name,age) {
    var p = {
        name:"tom",
        age:12,
        setName:function(name){
            this.name = name;
        }
    };
    return p;
}

createPerson('zs',12);
createPerson('ls',12);

4.自定义构造函数模式

自定义构造函数,通过new创建对象;适用于需要创建多个类型确定的对象的场合;每个对象都有相同的数据,浪费内存,可以使用构造函数 + 原型的模式;

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

var p1 = new Person('zs',12);
p1.setName('Jack');

1)构造函数 + 原型

在构造函数中只初始化一般函数,方法定义在原型上。

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

Person.prototype.setName = function(name) {
    this.name = name;
}
var p1 = new Person('zs',12);
p1.setName('Jack');

2)构造函数实例化原理

new一个对象这种方式叫做构造函数执行模式,此时的new的东西不仅是一个函数名,被称为,而返回的结果是一个对象,称为实例,而函数体中出现的this都是这个实例;

对于构造函数Car,如果直接调用,此时构造函数内部的this是window;如果通过new关键字创建其实例,此时的this是实例化的对象car1

function Car(color,brand) {
    this.color = color;
    this.brand = brand;
}

Car();
var car1 = new Car('red','Benz');

构造函数被new实例化的时候,就相当于普通函数被执行的时候, 此时产生一个Car函数执行上下文AO(创建一个私有栈内存,形参赋值+变量提升),里面会自动保存一个属性this:{},值为一个空对象,随后在此对象中保存属性

this = {
    color:color,
    brand:brand
}

最后将这个this(创建出的实例对象)作为构造函数的返回值返回

function Car(color,brand) {
    //this = {
    //    color:color,
    //    brand:brand
    //}

    this.color = color;
    this.brand = brand;

    // return this;
}

在全局上下文中,由于执行了var car1 = new Car(),此时就会有变量car1,值为构造函数return的this对象,所以我们可以通过car1.color访问属性

GO = {
    Car:(function),
    car1:{
        color:'red',
        brand:'Benz'
    }
}

注意:

1.类是函数数据类型,实例是对象数据类型 

2.new执行的时候,如果手动return的是一个基本值,对返回的实例无影响,如果手动return的是一个引用数据类型,会把默认返回的实例给替换掉。

3.new时无论是否加小括号,都相当于把构造函数执行了,也创建了对应的实例,只不过不加小括号是不能传递实参的。let fn1 = new fn;

函数的三种角色对应的内存图解:

1.普通函数:走的是函数闭包作用域链的查找机制;

2.构造函数:创建出函数实例对象,可以根据原型链查找属性;

3.对象:将函数看作堆内存中的一片区域,使用键值对调用,访问属性;

以上三者之间没有必然联系

4f4e527de0a44ee382477647157a6837.png

十. 继承

继承 即子类继承父类的属性和方法

1.原型链继承

现有两个构造函数,现在需要实现子类型的构造函数Sub能够访问到父类型Supper的方法showsSuperProp,实现子类型继承父类型;

// 父类型
function Supper() {
    this.supperProp = 'Super';
}

Supper.prototype.showsSuperProp = function() {
    console.log(this.supperProp);
}

var supper = new Supper();
supper.showsSuperProp();


// 子类型
function Sub() {
    this.subProp = 'Sub';
}


Sub.prototype.showsSubProp = function() {
    console.log(this.subProp);
}

var sub = new Sub();
sub.showsSubProp();

要通过子类型访问到父类型原型上的方法,那么子类型的原型为父类型的实例对象,这样通过子类访问父类的方法只需顺着原型链查找到父类型的原型即可,即:

Sub.prototype = new Supper();

但此时子类sub.constructor属性为父类supper ,还需要设置子类原型的constructor属性为子类,即

Sub.prototype.constructor = Sub;

// 父类型
function Supper() {
    this.supperProp = 'Super';
}

Supper.prototype.showsSuperProp = function() {
    console.log(this.supperProp);
}

var supper = new Supper();
supper.showsSuperProp();


// 子类型
function Sub() {
    this.subProp = 'Sub';
}

Sub.prototype = new Supper();
// 让子类型的原型的constructor指向子类型
Sub.prototype.constructor = sub;
Sub.prototype.showsSubProp = function() {
    console.log(this.subProp);
}

var sub = new Sub();
sub.showsSubProp();

原型链继承图解:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_18,color_FFFFFF,t_70,g_se,x_16

js中的继承的特点:

1.js继承不是拷贝继承(即子类继承父类会把父类中的属性和方法拷贝一份到子类中,供子类的实例调取)而是查找型继承(将父类的原型放到子类实例的原型链上);

2.子类可以重写父类的方法,这样会导致父类的其他实例也会受到影响

3.父类中私有或者共有的属性方法,最终都会变为子类中共有的属性或者方法;

2.借用构造函数继承

实现思路:在子类型的构造函数中调用父类型的构造,通过call()调用父类型的构造函数,把父类型的构造函数当作普通函数使用,让父类中的this指向子类的实例,相当于给子类设置了许多私有的属性和方法;

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

function Student (name,age,price) {
    Person.call(this,name,age);  // 相当于调用this.Person(name,age)
    this.price = price;
}

var s = new Student('tom',19,123456);
console.log(s.name,s.age,s.price);

call继承的特点:

1. 只能继承父类私有的属性和方法(因为是把父类当作普通函数调用和其原型上的属性和方法没有关系);

2.父类私有的变为子类私有的;

3.组合继承

使用原型链+借用构造函数的组合继承。利用原型链实现父类型对象方法的继承;利用call()得到父类型对象属性的继承,这两者结合使用才是完整的继承方法

// 父类型
function Supper(supperProp) {
    this.supperProp = 'Super';
}

Supper.prototype.showsSuperProp = function() {
    console.log(this.supperProp);
}

var supper = new Supper();
supper.showsSuperProp();


// 子类型
function Sub(supperProp,subProp) {
    Person.call(this,supperProp);
    this.subProp = 'Sub';
}

Sub.prototype = new Supper();
// Sub.prototype = Object.create(Supper.prototype);
// 让子类型的原型的constructor指向子类型
Sub.prototype.constructor = sub;
Sub.prototype.showsSubProp = function() {
    console.log(this.subProp);
}

var sub = new Sub();
sub.showsSubProp();

可以通过子类型修改父类型的原型上引用值的属性和方法;

不能通过子类型修改父类型的原型上原始值的属性和方法,但子类型上会因此添加这个同名属性;

十一.Web Workers

是h5提供的一个js多线程解决方案,我们可以将一些计算量大的代码交由web worker 运行而不冻结用户界面。但是子线程完全受主线程控制,且不得操作dom,所以这个新标准没有改变js单线程的本质。

使用:

主线程api

// 创建一个worker对象并向它传递在新线程中执行的脚本的URL
var worker = new Worker(url);
// 向分线程发送消息
worker.postMessage(msg);
// 接收worker传递过来的数据函数
worker.onmessage = function(event) {
    // event.data获取返回的数据
    var data = event.data;
}

分线程api

在分线程中,全局对象不是window,而是worker对象,所以在分线程中不需要再使用worker调用具体方法,直接调用webworker的api即可;分线程this打印输出如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 正是如此,在分线程中无法调用window上的方法,比如操作dom元素,调用alert方法

// onmessage获取主线程传递的数据
var onmessage = function(event) {
    // event.data能拿到主线程的数据
    var number = event.data;
}

// 分线程向主线程返回数据
postMessage(result);

示例:输入斐波那契数列的项数,返回这一项的值

求斐波那契数列第n项元素的需要使用递归,递归内存开销大,执行速度慢,所以在主线中获取输入的项数,将输入内容传递给子线程,将递归操作放在子线程中进行,计算出结果后返回主线程。

index.html

<body>
    <input type="text" placeholder="数值" id="number">
    <button id="btn">计算</button>
    <script type="text/javascript">
        var input = document.getElementById('number');
        document.getElementById('btn').onclick = function() {
            let number = input.value; 
            // 创建一个worker对象并向它传递在新线程中执行的脚本的URL
            var worker = new Worker('worker.js');
            // 向分线程发送消息
            worker.postMessage(number);
            // 接收worker传递过来的数据函数,
            worker.onmessage = function(event) {
                console.log(`主线程接收到分线程的数据${event.data}`);
                // 分线程返回的数据
                alert(event.data);
            }
            input.value = '';
        }
    </script>
</body>

子线程worker.js

function fibonacci(n) {
    return n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n -2);
}
console.log(this);
var onmessage = function(event) {
    // 分线程接收到主线程的数据
    var number = event.data;
    console.log(`分线程接收到主线程的数据${number}`);
    var result = fibonacci(number);
    console.log(`分线程返回到主线程的数据${result}`);
    // 分线程向主线程返回数据
    postMessage(result);
}

图解

主线程可以向分线程发送多个数据,分线程会使用消息队列存放多个消息,依次执行;主线程也可能接收多个分线程的消息,也会维护一个消息队列存放多个回调函数,一次取出执行。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

缺点

1)不是所有浏览器兼容Web Workers;

2)无法访问dom;

3)不能跨域加载js;

4)速度慢(使用web worker的目的是为了不让主线程执行繁重的计算任务,不冻结界面,从而交给分线程,但主分线程数据的传递也是需要时间的,所以不是使用web worker代码的整体执行速度就一定提高了)。

十二.websocket

服务器主动推送消息给浏览器的技术

1.客户端websocket实现

websocket是HTML5的新特性之一,首先你的浏览器必须支持websocket

1.创建WebSocket实例

const ws = new WebSocket('ws:localhost:8000');

参数url:ws://ip地址:端口号/资源名

2.WebSocket对象包含以下事件

open:连接建立时触发

message:客户端接收服务端数据时触发

error:通信发生错误时触发

close:连接关闭时触发

3.WebSocket对象常用方法

send():使用连接给服务端发送数据

客户端websocket代码模板:

;((doc,WebSocket) => {

    const msg = doc.querySelector('#msg');  // 获取输入框,需要发送的消息
    const send = doc.querySelector('#send');  // 发送按钮

    // 创建websocket实例
    const ws = new WebSocket('ws:localhost:8000');
    
    // 初始化
    const init = () => {
        bindEvent();
    }
    
    // 绑定事件
    function bindEvent () {
        send.addEventListener('click',handleSendBtnClick,false);
        ws.addEventListener('open',handleOpen,false);
        ws.addEventListener('close',handleClose,false);
        ws.addEventListener('error',handleError,false);
        ws.addEventListener('message',handleMessage,false);

    }

    function handleSendBtnClick () {
        const message = msg.value;
        
        // 将数据发送给服务器
        ws.send(JSON.stringify({
            message:message
        }));

        msg.value = '';
    }

    function handleOpen () {
        console.log('open');
        // 当连接建立时,一般做一些页面初始化操作
    }
    
    function handleClose () {
        console.log('close');
        // 当连接关闭时
    }

    function handleError () {
        console.log('error');
        // 当连接出现异常时
    }

    function handleMessage (e) {
        // 在这里获取后端广播的数据,数据通过事件对象e活得,数据存放在e.data中
        const showMsg = JSON.parse(e.data);
    }

    init();
})(document,WebSocket)

2.服务端socket实现

服务器还是基于node服务器进行,这里使用ws库,首先安装:

npm i ws --save

 1.创建Ws实例

const server = new Ws.Server({port:8000});

参数port:用于监听客户端传输数据的服务器端口号

2.Ws事件

open:连接建立时触发

close:连接关闭时触发

error:通信发生错误时触发

connection:连接建立成功且通信时触发

message:服务端接收客户端数据时触发

示例:

const Ws = require('ws');

;(() => {
    const server = new Ws.Server({port:8000});

    const init = () => {
        bindEvent();
    }

    function bindEvent () {
        server.on('open',handleOpen);
        server.on('close',handleClose);
        server.on('error',handleError);
        server.on('connection',handleConnection);
    }

    function handleOpen () {
        console.log('ws open');
    }
    function handleClose () {
        console.log('ws close');
    }
    function handleError () {
        console.log('ws error');
    }
    function handleConnection (ws) {
        console.log('ws connected');
        // message事件在connection事件中绑定
        ws.on('message',handleMessage);
    }
    function handleMessage (msg) {
        console.log('ws message',msg.toString());
        // 遍历当前连接的所有客户端,将信息广播出去
        server.clients.forEach((c) => {
            // 调用send方法推送消息给客户端
            c.send(msg.toString());
        })
    }

    init();
})()

十三.es6新特性

1.let和const

都可以声明变量,不同点在于:

  • let不存在变量提升,当前作用域中不能在let声明前使用变量;
  • 同一个作用域中,let不允许重复声明
  • let解决了typeof的一个暂时性死区的问题
  • 全局作用域中,使用let声明的变量并没有给window加上对应的属性
  • let会存在块作用域,除对象以外的大括号都可以被看成块级私有作用域;
  • 基于const创建的变量,变量的存储的值不能被修改(常量)

2.箭头函数及this

es6中新增了创建函数的方式,箭头函数,项目中是箭头函数和function普通函数混合使用;

箭头简化了创建函数的代码,箭头函数创建如下:

function sum (n,m) {
    return n + m;
}

// 该写成箭头函数
let sum = (n,m) => {
    // 函数体
    return n + m;
}

箭头函数的创建都是函数表达式的方式(变量=函数),这种模式下不存在变量提升,函数只能在创建完成后执行。


箭头函数书写注意点: 

1.如果函数体中只有一行return,可以省略return和大括号

let sum = (n,m) => n + m;

2.箭头函数如果只有一个形参,可以省略圆括号。用箭头函数简化下列柯里化函数。

function fn (n) {
    return function (m) {
        return n + m;
    }
}

let fn = n => m => n + m;

3.箭头函数中没有arguments,但是可以使用剩余运算符获取传递的实参集合,这个集合是一个数组;

es6支持给形参设置默认值

let sum = (constext = window,...arg) => {
    console.log(eval(arg.join('+')));
}

sum(obj,1,2,3,4);

4.箭头函数中没有this,其中使用的this都是自己所处上下文中的this,使用call无法改变箭头函数中的this指向。

window.name = 'window';
let fn = n => {
    console.log(this.name);
}

let obj = {
    name:'obj',
    fn:fn
};

fn(10);  // window 
fn.call(obj,10);  // window

document.body.onclick = fn;  // window
obj.fn(10);  // window

3.解构赋值

让左侧出现和右侧值相同的结构,以此快速获取到我们需要的内容(数组和对象的解构赋值最常用)

1)数组的结构赋值

有如下代码

let array = [10,20,30,40,50];
let n = array[0];
let m = array[1];
let x = array.slice(2);

以上代码可以使用解构赋值的的方式实现,其中...x是扩展运算符,将剩下的内容存储到变量x中,它只能出现在最后。

let [n,m,...x] = array; // n = 10 m = 20 x = [30,40,50]

如果数组中某项不需要获取,可以使用如下方式

let [n,,m] = array;  // n = 10 m = 30

如果数组中没有某一项,可以使用等号赋予默认值

let [n,,m,,,x = 0] = array;

多维数组的解构赋值

let array = [10,[20,30,[40,50]]];
let [n,[,,[,m]]] = array; // 10 50

2)对象的解构赋值

要求创建的变量名要和对象的属性名一致

let obj = {
    name:'漩涡鸣人',
    age:30,
    sex:'男',
    friends:['佐助','鹿丸']
};

let {name,age,sex} = obj;

对象结构的重命名,冒号相当于给获取的结果设置一个别名(变量名),下面的例子就创建可一个叫nianling的变量存储obj.age的值;

let {age:nianling} = obj;
console.log(niannling);

对象解构的默认值

let {height = '180cm'} = obj;
console.log(height);  // 180cm

多维对象获取

let {name,friends:[firstFriend]} = obj;
console.log(name,firstFriend); // 漩涡鸣人 佐助

4.“...”运算符的作用

1)扩展运算符(多用于解构赋值中)

let [n,...m] = [12,34,67]; // n = 12 m = [34,67]

2)展开运算符

  • 传递实参
let array = [12,34,56,78,89,90];
let min = Math.min(...array);
  • 数组/对象的克隆
let array = [12,34,56,78,89,90];
let cloneArray = [...array];

let obj = {name:'xxx',age:80};
let cloneObj = {...obj,sex:'female',age:90};

3)剩余运算符(多用于接收实参),获取的是一个数组

let fn = (n,...arg) => {
    // n = 10  arg:[20,30]
}

fn(10,20,30);

5.class创建类

es5使用function创建一个类,在es6中可以使用class关键字创建一个类;

1)constructor等价于构造函数体;

2)直接写在class内部的方法是加载Fn的原型上的;

3)前面设置static的把当前Fn当作普通对象设置键值对;

class Fn {
    // 等价于构造函数体
    constructor(n,m) {
        this.x = 100;
    }

    // 直接写的方法是加载Fn的原型上的
    getX() {
        console.log(this.x);
    }

    // 前面设置static的把当前Fn当作普通对象设置键值对  
    static queryX() {}
}

// 也可以class外部添加原型方法
Fn.prototype.getY = function () {}

let f = new Fn(10,20);
f.getX();
Fn.queryX();

注意:class创建的类只能用new执行,不能当作普通函数执行;

1)es6中类的继承

使用extends关键字实现类的继承,class child extends parent {},相当于执行了child.prototype.__proto__ = parent.prototype;

子类只要继承父类,可以不写constructor,一旦写了,则在constructor中的第一句话必须是super(),使用super相当于call继承,把父类私有属性继承给子类;若不写constructor浏览器会自己默认创建constructor,constructor(...args) { super(...args) }

class A {
    constructor (x) {
        this.x = x
    }
    getX () {
        console.log(this.x);
    }
}

class B extends A {
    constructor (y) {
        super();   // A.call(this);
        this.y = y
    }
    getY () {
        console.log(this.y);
    }
}

6.模板字符串

解决原本字符串和变量的拼接,模板字符串使用反引号(撇)在TAB键上面,包裹字符串;

模板字符串中书写js表达式的方式 ${js表达式};

模板字符串内可以使用换行,一般用来拼接html模板。

let year = 2022;
let month = 1;
let str = `今天是${year}年,${month}月`;

7.async/await

es7中新增的语法糖,函数中只要使用了await,则当前函数必须使用async修饰;

async是修饰一个函数,控制其返回的结果是一个Promise实例;

await可以理解为把一个异步操作修饰为同步结果(实际上还是异步),其后必须接一个Promise实例,若Promise正常处理,则其回调的resolve参数作为await表达式的值;若Promise处理异常,await表达式会把Promise的异常结果抛出(需要使用try...catch语句捕获或异常);若await后返回的非Promise,则返回该值本身。 


示例:有两个定时器,要求第一个执行完后再执行第二个

直接使用Promise的写法:

let res = new Promise(resolve => {
    setTimeout(() => {
        resolve(1);
    },1000)
});

res.then(res => {
    console.log(res); // 1
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(2)
        },1000);
    })
}).then(res => {
    console.log(res); // 2
})

使用async/await的写法:在Promise基础上简化写法

async function func () {

    let res;
    res = await new Promise(resolve => {
        setTimeout(() => {
            resolve(1);
        },1000);
    });

    console.log(res);  // 1
    
    res =  await new Promise(resolve => {
        setTimeout(() => {
            resolve(2);
        },1000);
    });

    console.log(res);  // 1
}

func();

8.生成器函数

在function关键字后加上 * ,此函数称为生成器函数,该函数的返回值是Generator对象(无论手动返回什么值都是如此),平时都配合yield来使用;

执行函数的时候只会创建一个生成器对象,方法中的代码不会执行;而是基于gt.next()执行函数;在执行的时候遇到yield就会暂停一次,此时next()方法会返回一个对象,对象中的value存储的是yield的结果,done标记的是此时是否将代码都执行完。

function * func () {
    console.log(1);
    yield 'A';    // 一般在这里执行异步操作,获取结果
    console.log(2);
    yield 'B';
    console.log(3);
    yield 'C';
}

let gt = func();  // 此时只创建一个生成器对象,但是方法中的代码还没有执行
// 基于gt.next执行函数 此时执行 console.log(1) 并返回一个对象 { value:'A',done:false }
console.log(gt.next()); 
console.log(gt.next()); // 2 { value:'B',done:false }
console.log(gt.next()); // 3 { value:'C',done:false }
console.log(gt.next()); // { value:undefined,done:true }

猜你喜欢

转载自blog.csdn.net/weixin_43655896/article/details/123095649