/OO

Primary LanguageHTML

Javascript 面向对象

数据类型

简单数据类型: string number boolean undefined

复杂数据类型:Object Array Date RegExp function string Number Boolean Math Null

变量预解析

js代码在执行之前 会在相应的执行环境中 预先把 一些东西解析到内存

js解析器在运行js代码的时候 分为两步:

  • 第一步 把所有的 变量、函数、参数 提前到当前作用域的顶部。
    • 只提前变量,不提前值
    • 方法是整体的提升
  • 第二步 逐行解读代码 从左到右、从上至下。
console.log(a); // undefined
var a = 111;

// 前面这段代码等于
var a;
console.log(a); // undefined
a = 111;
//方法整体提升
// 1.先运行fun();
// 2.提前变量,var num;
// 3.console.log(num);  undefined
// 4.num=20
// 5.var num=10 最外面
var num = 10;
fun();  // undefined
function fn(){
    console.log(num);
    var num = 20;
}
// 1.函数提前
// 2.var a=9; b=9; c=9 只有a是局部变量,其他都是全局变量
// 3.外面执行不了函数里面的局部变量 a , 报错
f2();
console.log(c); // 9
console.log(b); // 9
console.log(a); // 报错

function f2(){
    var a=b=c=9;
    console.log(a); //9
    console.log(b); //9
    console.log(c); //9
}

闭包

闭包的概念

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。阮一峰

  • 闭包原理:上级作用域无法访问下级作用域 , 而下级作用域可以访问上级作用域
function f1(){
    var num = 1;
    function f2(){
        console.log(num);
    };
    // 在局部作用域找f2 , 能够找到
    f2();
}
// 如果直接在外部调用 , 就是在全局作用域找 f2 , 报错
// f2();    // error
f1();   
  • 闭包要解决什么问题
    • 函数内部的函数中可以访问该函数变量
    • 但是我们需要在函数外部中访问函数内部变量
// 1.说明每次function 返回的值都是新值
function foo(){
    var obj = {
        name:'Bin',
        age:'21'
    }
    return obj;
}
var obj1 = foo();
var obj2 = foo();
// 因为new一个Obj 就是在内存中新创建一个对象 .. 实际上创建了两个对象
console.log(obj1 == obj2);  // false

// 2.说明每次function 返回的值都是新值
    function foo(){
        var num = 123;
        return num;
    }
    var x = foo();
    var num = x+x;
    console.log(num);

    var y = foo();
    // 还是获得 123
    console.log(y);

// 3.但是我们需要在函数外部中访问函数内部变量   
    function foo(){
        var num = 234;
        function inner(a){
            num = a;
            console.log(num);
        }
        //1. 如果我们要改变num , 可以在内部改变
            // inner(1);
            // inner(2);

        // 2.如果要在外部改变里面的值 , 怎么办?
        return inner;
    }
    // 3.返回的是 inner();
    var innerFun = foo();
    // 4.此时就可以在外部访问内部的函数并赋值
    innerFun(10);
  • 闭包基本模式

    • 在外部函数(foo)内部创建函数(inner) , 在这个函数内部中(inner)可以操作foo中的数据 (inner可以修改num)
    • 将外部函数中的返回值 设置为 内部函数 , (返回 inner)
    • 在外部调用 外部函数(foo) , 就可以接受到返回值 (inner)
    • 使用这个内部函数,就可以在外部 将 外部函数里的变量(num) 进行修改
  • 缺点

    • 消耗更多的内存
function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

    return f2;

}

var result=f1();

result(); // 999

nAdd();

result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

阮一峰

错误处理

错误一定会发生

当 JavaScript 引擎执行 JavaScript 代码时,会发生各种错误: 可能是语法错误,通常是程序员造成的编码错误或错别字。 可能是拼写错误或语言中缺少的功能(可能由于浏览器差异)。 可能是由于来自服务器或用户的错误输出而导致的错误。 当然,也可能是由于许多其他不可预知的因素。

JavaScript 抛出错误

当错误发生时,当事情出问题时,JavaScript 引擎通常会停止,并生成一个错误消息。

描述这种情况的技术术语是:JavaScript 将抛出一个错误。

JavaScript 测试和捕捉

try 语句允许我们定义在执行时进行错误测试的代码块。

catch 语句允许我们定义当 try 代码块发生错误时,所执行的代码块。

JavaScript 语句 try 和 catch 是成对出现的。

错误对象

  • SyntaxError:语法错误
  • ReferenceError:引用错误,要的变量没找到
  • TypeError:类型错误 , 把变量当方法使用, 把方法当变量使用
    • 错误的使用了类型
    • 错误的调用了类型的方法
  • RangeError:范围错误 , 参数超范围

重载

所谓重载,简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。所以说,重载主要需要两点:第一,同样的函数名。第二,不同的函数参数。

javascript不支持重载,但是可以模仿函数的重载

arguments

  • 每个函数自带 arguments
  • 传入的参数值是 类数组对象 , 是对象
  • 类数组 与 数组相同点 :
    • 都有下标
    • .length
  • 类数组 与 数组不同点
    • arguments是对象类型
    • 数组是Array
    • 不能互用API

arguments的属性

  • arguments.length 获取传参的长度
  • arguments.callee 指向函数本身
    function test(x,y){
        if(arguments.length !== 2);{
            throw new Error('传参过多')
        }
        // 自己调用自己 称为递归
        test();
    }
    test('1','2','3');

    // 但是匿名函数如何调用自己呢?
    (function(){
        console.log('调用自己');
        // 不能在严格模式下进行
        arguments.callee();
    })()

匿名函数

匿名函数特征

  • 不指定函数名
  • 用匿名函数节约资源,用完就销毁

概念:定义函数的方式

  • 函数的声明
  • 函数表达式

如下代码就是函数声明的代码结构:

function sum(x,y){  
    console.log(x+y);  
}  
sum(1,2); //3 

函数表达式

var ss = function(x,y){  
    alert(x+y);  
};  
ss(1,2);  

面向对象 OOP

首先,先理解一下对象,很多事物都是对象,简单到一整数,复杂到一架飞机,对象是一个整体,对外提供一些操作;那么面向对象就是说,使用对象的时候,你可以直接使用它所提供的功能而忽略其内部组成情况。面对对象不一定只有在编程界里才有,我们生活中无处不在;我的理解是这样的:比如说,你家里的电视机,你使用了遥控,就能操作电视机,但是你实际上不知道这台电视机里面是什么零件组成的,你只要知道,我拿到遥控就可以操作电视机就好了。这就是一种面向对象的**。

特点

  • 抽象
    • 抓住核心问题
  • 封装
    • 不用考虑内部实现过程,只要考虑功能的使用
  • 继承
    • 从已有对象,继承出新的对象
  • 便于维护
    • 先将事物的属性和方法封装在一个对象,再反复使用对象这个功能

三种方式创建对象

  1. 直接量
    • 在创建对象时,就知道所有成员,用直接量创建
// 何时使用这种创建方式?
// 在创建对象时,就知道所有成员,用直接量创建
var lilei = {
    name:'李雷',
    age:11,
    intr:function(){
        // `${变量}`  ES6语法
        console.log(`我是${this.name},我${this.age}岁了`);
    }
}
console.log(lilei);
lilei.intr();
  1. 用 new 创建
    • JS即使对象创建完成 , 依然可以添加新的成员
// 何时使用这种创建方式?
// 在创建对象时,不知道所有成员,用 new Object
// JS即使对象创建完成 , 依然可以添加新的成员
var lilei = new Object();
lilei.name = '李雷';
lilei.age = 11;
lilei.intr=function(){
    console.log(`我是${this.name},我${this.age}岁了`);
}
// JS一切都是关联数组
// obj.name == obj["name"]
// 对象存储结构和关联数组完全一样
console.log(lilei["name"]);
console.dir(lilei);
  1. 构造函数
    • 描述一类对象统一结构的函数
    • 反复创建多个相同类型的结构的对象时使用
    • 构造函数首字母大写
    • 如何使用构造函数
    • 优点: 解决了代码重用
    • 缺点: 无法节约内存
function student(name,age){
    this.name = name;
    this.age = age;
    this.intr = function(){
        console.log(`我是${this.name},我${this.age}岁了`);
    }
}
var lilei = new student('李雷',11);
var hmm = new student('韩梅梅',12);
lilei.intr();
hmm.intr();
console.log(lilei.intr == hmm.intr); //false

// 可以看出:

// 1.构造函数内部会创建一个实例,调用普通函数时则不会创建新的对象。

// 2.构造函数内部的this指向是新创建的person实例,而普通函数内部的this指向调用函数的对象(如果没有对象调用,默认为window)

继承

为什么要继承?

  • 因为需要复用
  • 节约内存

该如何复用?

通过复用我们可以实现共享。

从对象的角度来说,我们不想重复写同一段逻辑,所以逻辑需要复用;但是我们不希望一个对象掌管的变量被其它对象修改。所以变量不能共享。也就是要共享函数,不共享属性。

继承特点

  • 父对象的成员,子对象无须创建就可以直接使用
  • 只要多个子对象,拥有相同的属性和功能时,就要用到继承
  • 只要在父对象定义一次所有子对象共用
  • Js继承都是继承原型对象
    • prototype 原型
    • 原型对象是:集中存储一类子对象相同属性和功能的父对象
    • 子对象有共同属性和功能都要定义在原型对象中
    • 每创建一个构造函数,都会自动创建一个原型对象
    • 用new创建子对象时,会自动设置子对象的 __proto__继承构造函数的prototype
// 构造函数只写属性
function student(name,age){
    this.name = name;
    this.age = age;
}
// 相同方法写在原型对象中
student.prototype.intr = function(){
    console.log(`我是${this.name},我${this.age}岁了`);
}
// 相同固定属性写在原型对象中
student.prototype.className = '初一二班';
var lilei = new student('李雷',11);
var hmm = new student('韩梅梅',12);
lilei.intr();
hmm.intr(); 
console.log(lilei.intr == hmm.intr); //true 
console.log(lilei.className);
  • 自有属性和共有属性
    • 自由属性:直接保存在对象本地属性
    • 共有属性:保存在原型对象中 , 所有子对象共有共享的
    • 获取值时:对象.属性
    • 赋值
      • 自有属性:对象.属性=值;
      • 共有属性:构造函数.prototype.属性=值;
function student(name,age){
    this.name = name;
    this.age = age;
}
// 相同方法写在原型对象中
student.prototype.intr = function(){
    console.log(`我是${this.name},我${this.age}岁了`);
}
// 相同固定属性写在原型对象中
student.prototype.className = '初一二班';
var lilei = new student('李雷',11);
var hmm = new student('韩梅梅',12);
lilei.intr();
hmm.intr(); 
// 李雷修改了原型对象共有属性
lilei.className = '初三二班';
// 这样修改原型属性是错误
console.log(lilei.className); // 修改了自有属性
console.log(hmm.className);   // 原型共有属性不变
// 要想修改共有属性:
student.prototype.className = '初三二班';
console.log(lilei.className);
console.log(hmm.className); 
* 用来判断是否自有属性API obj.hasOwnProperty('属性名')
function student(name,age){
    this.name = name;
    this.age = age;
}
// 相同方法写在原型对象中
student.prototype.intr = function(){
    console.log(`我是${this.name},我${this.age}岁了`);
}
// 相同固定属性写在原型对象中
student.prototype.className = '初一二班';
var lilei = new student('李雷',11);
var hmm = new student('韩梅梅',12);

// 检查属性
function checkPropo(obj,name){
    // 返回布尔值
    if(obj.hasOwnProperty(name)){
        console.log('自有属性');
    }else if(obj[name] !== undefined){
        console.log('共有属性');
    }else if(obj[name] == undefined){
        console.log('没有属性');
    }
}
checkPropo(lilei,'name');   // 自有
checkPropo(hmm,'className');// 共有
checkPropo(hmm,'abc');      // 没有   
* in 判断属性是否在对象中 , 只要自己或者父对象包含 属性名 , 就返回 true
function student(name,age){
    this.name = name;
    this.age = age;
}
// 相同方法写在原型对象中
student.prototype.intr = function(){
    console.log(`我是${this.name},我${this.age}岁了`);
}
// 相同固定属性写在原型对象中
student.prototype.className = '初一二班';
var lilei = new student('李雷',11);
var hmm = new student('韩梅梅',12);

// 检查属性
function checkPropo(obj,name){
    // 返回布尔值
    if(obj.hasOwnProperty(name)){
        console.log('自有属性');
        // 如果 属性 有 在对象里面
    }else if(name in obj){
        console.log('共有属性');
    }else if(obj[name] == undefined){
        console.log('没有属性');
    }
}
checkPropo(lilei,'name');   // 自有
checkPropo(hmm,'className');// 共有
checkPropo(hmm,'abc');      // 没有   

内置对象的继承关系

JavaScript有3大对象,分别是本地对象内置对象宿主对象

  • 本地对象
    • 与宿主无关,独立于宿主环境的ECMAScript实现提供的对象。
    • 简单来说,本地对象就是 ECMA-262 定义的类(引用类型)。
    • 这些引用类型在运行过程中需要通过new来创建所需的实例对象。
    • 包含:ObjectArrayDateRegExpFunctionBooleanNumberString等。
  • 内置对象
    • 与宿主无关,独立于宿主环境的ECMAScript实现提供的对象。
    • 在 ECMAScript 程序开始执行前就存在,本身就是实例化内置对象,开发者无需再去实例化。
    • 内置对象是本地对象的子集。
    • 包含:Global和Math。
    • ECMAScript5中增添了JSON这个存在于全局的内置对象。
  • 宿主对象
    • 由 ECMAScript 实现的宿主环境提供的对象,包含两大类,一个是宿主提供,一个是自定义类对象。
    • 所有非本地对象都属于宿主对象。
    • 对于嵌入到网页中的JS来说,其宿主对象就是浏览器提供的对象,浏览器对象有很多,如Window和Document等。
    • 所有的DOMBOM对象都属于宿主对象。

关于专业名词:本地对象也经常被叫做原生对象或内部对象,包含Global和Math在内的内置对象在《JavaScript高级程序设计》里也被叫做单体内置对象,很多时候,干脆也会直接把本地对象和内置对象统称为“内置对象”,也就是说除了宿主对象,剩下的都是ECMAScript的内部的“内置”对象。

// 就是你打开浏览器时 , js 自动创建的构造函数
// arr就是继承关系 继承了 Array()的原型对象
// Array()原型对象放了 sort() push() slice() .. 等等方法 , 所以可以直接调用

var arr = new Array();
var date = new Date();
// __proto__ 指向了原型对象,里面内置了很多API
console.log(arr);
var arr2 = new Array();
console.log(arr == arr2);   // fase
// 证明都指向相同的原型对象
console.log(arr.__proto__ == arr2.__proto__);   // true 

解决浏览器兼容问题需要用到内置对象

// 如果一个API新的浏览器支持 , 旧浏览器不支持怎么办?
// 新的API 数组 的 indexOf

var arr = [1,2,3,2,1];
// 浏览器兼容 IE8
if(typeof(Array.prototype.indexOf) != 'function'){
    Array.prototype.indexOf = function(val,fromi){

        // 如果fromi 用户没有给, 默认为0
        fromi = fromi || 0;
        // i从fromi遍历当前数组 -> this
        // this 将来调用API的 . 前的对象 , 现在这个对象值arr,因为 arr调用他
        for(var i = fromi;i<this.length;i++){
            // 如果当前数组的当前元素值为 val
            if(this[i] === val){
                // 返回i
                return i;
            }
        }
        遍历结束都没有 返回-1
        return -1;

    }
}
console.log(arr.indexOf(2));    // 有值返回 1
console.log(arr.indexOf(2,2));  // 有值返回 3
console.log(arr.indexOf(2,4));  // 没值返回 -1  

原型链

  • 原型链由多级父元素逐级继承形成的链式结构
  • proto 只能继承父级的 Prototype
  • 原型链保存着所有对象成员和方法
  • 作用域链保存着所有变量 , 控制变量使用顺序,优先使用AO局部变量, 没有才向作用域链父级作用域查找 , 作用域链顶端 window
  • 原型链控制着对象成员的使用顺序,优先使用自己的方法,没有才顺着原型链向父级查找
  • 原型链顶端一定是 Object.prototype
  • 概括
    • 所有不需要"对象."访问的变量都保存在作用域链
    • 所有必须要"对象."访问的成员都保存在原型链

proto 与 prototype

  • 每个对象都具有一个名为__proto__的属性;
  • 每个构造函数(构造函数标准为大写开头,如Function(),Object()等等JS中自带的构造函数,以及自己创建的)都具有一个名为prototype的方法(注意:既然是方法,那么就是一个对象(JS中函数同样是对象),所以prototype同样带有__proto__属性);
  • 每个对象的__proto__属性指向自身构造函数的prototype;

proto 与 prototype proto 与 prototype

原型链图解: 原型链

逻辑清晰版本原型链图解:

原型链

判断一个对象是不是数组?有几种方法

  1. typeof
    • 结论:typeOf 只能识别原始类型 , function 和 object
    • 无法识别 Object 中不同的对象类型
var n=1,s="str",b=true,nu=null,un=undefined;
function fn(){};
var obj1={};
var arr=[];

// 结论:typeOf 只能识别原始类型 , function 和 object
// 无法识别 Object 中不同的对象类型
console.log(
typeof(n),
typeof(s),
typeof(b),
typeof(nu), // null = object 
typeof(un),
typeof(fn),
typeof(obj1),
typeof(arr) // arr = object
);  
  1. 验证原型对象
    • 验证是否数组对象: Object.getPrototypeOf(对象) == Array.prototype
    • var bool = father.prototype.isPrototypeOf(child)
var n=1,s="str",b=true,nu=null,un=undefined;
function fn(){};
var obj1={};
var arr=[];
// 1.检查原型对象
    console.log(
        // 获得Obj1的原型对象 与 数组对象的原型对象做比较
        Object.getPrototypeOf(obj1) == Array.prototype,
        // 获得arr的原型对象 与 数组对象的原型对象做比较
        Object.getPrototypeOf(arr) == Array.prototype
    );
// 2 var bool = father.prototype.isPrototypeOf(child)
    console.log(
        Array.prototype.isPrototypeOf(obj1),
        Array.prototype.isPrototypeOf(arr),
    );  
  1. 验证构造函数
    • 构造函数自动生成 原型对象 prototype
    • 子对象称呼 父级原型对象 proto
    • 那原型对象称呼构造函数为: constructor (构造器)
    • 验证API
      • obj1.constructor == Array
      • var bool = obj instanceof Array (判断Obj是否由构造函数Array创建出来)
var n=1,s="str",b=true,nu=null,un=undefined;
function fn(){};
var obj1={};
var arr=[];
    // - 构造函数自动生成 原型对象 prototype
    // - 子对象称呼 父级原型对象 __proto__
    // - 那原型对象称呼构造函数为: constructor (构造器)
    console.log(
        // 判断构造函数是否等于 数组对象
        obj1.constructor == Array,
        arr.constructor == Array,
    );  

    console.log(
        // 判断构造函数是否等于 数组对象
        // instanceof 生来就是验证与判断
        // 判断Obj是否由构造函数Array创建出来
        // var bool = obj instanceof Array
        obj1 instanceof Array,
        arr instanceof Array,
    );  

    // instance 翻译:实例 , 一个类型中具体的一个对象
    // 学生是一个类 , 李雷是一个实例对象
    // 实例化就是用New创建一个对象
    // obj instanceof Array
    // obj 是不是Array类型的 一个实例
    // 从原型判断
        // 翻译: arr的原型是指向数组原型吗
        var result = Array.prototype.isPrototypeOf(arr);
        console.log(result);
        // 翻译:arr的原型是等于数组原型吗
        console.log(Object.getPrototypeOf(arr) == Array.prototype);
    // 从构造函数判断
        // constructor 构造函数
        // 翻译: arr的构造函数 是不是 Array
        console.log(arr.constructor == Array);
        // arr是不是Array这个构造函数的实例化
        var bool = arr instanceof Array;
        console.log(bool);  
  1. 判断内部属性Class
    • 验证原型 与 验证构造函数 还是有一定缺点的
    • 验证不够严谨 ,即使创建时不是数组对象只要原型链上有数组类型,也认为是数组类型对象此时,就算obj可以调用到数组API也是无意义的
var n=1,s="str",b=true,nu=null,un=undefined;
function fn(){};
var obj1={};
var arr=[];
var obj3 = {};
// obj3的原型对象指定 arr , 那么 这关系就变成了
// obj3.__proto__ -> arr.__proto__ -> Array.prototype
// 也就是obj3 可以用 数组API
obj3.__proto__ = arr;
console.log(
    // 判断构造函数
    obj3 instanceof Array,  // true
    // 判断原型对象
    Array.prototype.isPrototypeOf(obj3) // true
    // 总结:验证不够严谨 ,即使创建时不是数组对象
    //      只要原型链上有数组类型,也认为是数组类型对象
);
  1. 检查内部属性class

    • class是每个对象中记录对象创建时使用的类型属性
    • 一旦对象被创建,class属性就无法被修改
  2. Array.isArray(obj);

    • 专用判断是否数组API ES5 新增的函数
var n=1,s="str",b=true,nu=null,un=undefined;
function fn(){};
var obj1={};
var arr=[];
var obj3 = {};
obj3.__proto__ = arr;
console.log(
    // ES5 新增 , 实际上就是用了 Object.prototype.toString.call()
    Array.isArray(arr),
    Array.isArray(obj1),

);  

什么是内部属性?

  • 内部属性是不可轻易访问
  • 只能通过 Object.toString() 这个原型对象获取
  • 只有 var obj = {} , 最纯正的对象可以获取到 , 其余都不行
  • var arr = [1,2] , arr.toString(); 访问到的是数组装的内容
    • 为什么出现这种状况?
    • 按原型链 , Object.prototype 是最顶层 , 无论 Array , Date , RegExp 都会继承到toString()
    • 因为 Array.prototype 改写了toString()方法,改写成适合自己的方法
    • 在数组中,用户会迫切想知道数组里面装的是什么内容 , 而并不关心 array的对象类型
    • 所以 array 为了迎合用户 , toString()变成了输出内容
    • 以上内容 , 就叫多态 , Js 面向对象第三大特征
// 此时,就算obj3可以调用到数组API也是无意义的
// 检查内部属性 class , class是每个对象中记录对象创建时使用的类型属性
// 一旦对象被创建,class属性就无法被修改 
// 内部属性无法轻易获得
// 唯一办法调用 Object.prototype中的 toString();
console.log(obj1.toString());
// 输出结果 [object Object] 
    // 第一个小写object 表示引用类型对象
    // 第二个大写Object 表示Class内部属性

console.log(arr.toString());    // 空字符串
console.log(obj3.toString());   // 空字符串
// arr.toString() 为什么会出现空字符串?
    // 因为 Array.prototype 改写了 toString() 这个方法
    // 当你获得一个数组的时候,最想知道的是数组里面装了什么内容
    // 并不是最想知道这个 对象 是什么类型
    // 所以, toString() 经过 Array 的改造 , 变成了输出内容
    // 以上内容就叫做 多态 , 面向对象第三大特点

多态

  • 同一个事物,在不同的情况下表现出不同的状态
  • 重写 overRide
  • 如果子对象觉得父对象的成员不好用,可以在子对象本地定义同名成员来覆盖父对象成员
  • 只要觉得父对象成员不好用,那么就可以在本地重写父对象的成员
  • 重写,就是直接定义同名方法
function student(name,age){
    this.name = name;
    this.age = age;
}
// 相同方法写在原型对象中
student.prototype.intr = function(){
    console.log(`我是${this.name},我${this.age}岁了`);
}
var lilei = new student('李雷',11);
// 李雷调用 toString()会得到什么?
console.log(lilei.toString());
    // [object Object] 
    // 因为李雷的prototype并没有定义toString()
    // 顺着原型链找到了 obecjt.prototype 的 toString()方法

// 这样等于重写父对象toString()方法
student.prototype.toString = function(){
    console.log(`重写OverRide,我叫${this.name}`);
}   
lilei.toString();
  • 几乎所有内置对象的原型都重写了 Obeject toString()方法,导致所有内置对象的子对象,都无法调用到 ObjecttoString()
    • 解决办法 call()

call()

使用方法:父对象.prototype.调用方法名.call(调用对象)

var n=1,s="str",b=true,nu=null,un=undefined;
function fn(){};
var obj1={};
var arr=[];
var obj3 = {};
obj3.__proto__ = arr;
console.log(
    arr.toString(), // 空,被Array拦截
    obj3.toString(),// 空,被Array拦截
);
// call 格式: 父对象.prototype.调用方法名.call(调用对象)
console.log(
    Object.prototype.toString.call(arr), // [object Array]
    Object.prototype.toString.call(obj3),// [object Object]
);
// 判断
console.log(
    Object.prototype.toString.call(arr) == "[object Array]", // true
    Object.prototype.toString.call(obj1) == "[object Array]", // false
    Object.prototype.toString.call(obj3) == "[object Array]", // false
);

自定义继承

仅设置两个对象之间的继承关系

  • child.proto = father;
  • 问题是 proto 也是内置的属性 , 内部属性可能被浏览器禁用
  • 解决:Object.setPrototypeOf(child,father) 设置child 继承 father
var lilei = {
    name:'李雷',
    age:11,
    intr:function(){
        console.log(`我是${this.name},我是${this.age}岁`);
    }
}
lilei.intr();
// lilei.__proto__ -> Object.prototype
var father = {
    bal:1000000000,
    car:'bwm'
}
// 自定义继承
Object.setPrototypeOf(lilei,father);
console.log(lilei);

批量设置子对象继承关系

  • 只要修改构造函数的prototype对象即可
var father = {
    bal:100000000,
    car:'bwm'
}
function student(name,age){
    this.name = name;
    this.age = age;
}
// 先修改继承
student.prototype = father;
// 再添加方法
student.prototype.intr = function(){
    console.log(`我是${this.name},我${this.age}岁了`);
}
// 最后实例化对象
var lilei = new student('李雷',11);
var hmm = new student('hmm',12);
console.log(lilei);

两种类型间的继承

如果发现多个类型有相同的属性和相同的方法时,纪要抽象出一个父类型

  • 定义父类型
    • 构造函数:集中定义相同的属性结构
    • 原型对象:集中定义相同的方法
  • 让子类型原型对象基础父类型原型对象
    • 保证子对象可调用父类型原型中的方法
  • 让子类型构造函数中借用父类型构造函数
    • 请父类型构造函数 构建 共有的属性
    • 问题:直接调用父类型构造函数 , 那么 this 指向谁
    • this 默认 指向 windwo
    • 解决
      • call(); 只要this不是想要的,就用call随便替换
      • 父类型构造.call(this,参数) 替换this
// 父类型
function Flyer(name,speed){
    this.name = name;
    this.speed = speed;
}
// 父类型原型对象方法
Flyer.prototype.fly = function(){
    console.log(`${this.name}以时速${this.speed}飞行`);
}
// 子类型
function plane(name,speed,score){
    // 问题出在 this 上
    // this 获得的是 . 前面的对象
    // 没有 . 那么会自动获得创建的新对象
    // 那么没有用. 调用 也没有用 new 调用 , 那么 this指向window
    // 问题:直接调用父类型构造函数 , 那么 this 默认 指向 windwo
    // 解决 call(); 只要this不是想要的,就用call随便替换
    Flyer.call(this,name,speed);    // 借用父类型构造
    // 自己理解:
        // 构造函数在没有 New 或者没有 "." 的情况下 , This是指向window
        // 当你new一个构造函数时 , 创建一个新的对象,  this是指向新对象
        // 在plane 调用Flyer的时候 并没有 New 或者 "." , 所以指向window
        // 用call(),把新对象(plane)的this 替换 Flyer 里面的 this . 就等于指向了新对象
    this.score = score;
}
plane.prototype.getScore = function(){
    console.log(`${this.name}以时速${this.speed}飞行,击落我获得${this.score}分`);
}
// 子类型继承父类型 原型对象
Object.setPrototypeOf(plane.prototype,Flyer.prototype);
var j10 = new plane('j10',100,50);
console.log(j10);   
j10.getScore();
j10.fly();

图解: 两种类型之间的继承

this

关键字 this ,引用正在调用函数的对象的关键词

自己理解 :

  • 自动引用正在调用当前的 . 前的对象
  • 不加 this的变量,默认在作用域链中找,不会去对象中找
  • 只要对象的方法想使用自己的属性时 , 必须加this
  • obj.fun() , this 指向obj
  • new Fun() , this 指向正在创建的新对象
  • fun() 和 匿名函数自调, this 指向window
  • 当一个构造函数 , 没有 new 没有 . 那么 this指向window
  • 如果this 不是你想要的时候 , fun.call(this,参数) 替换

new Object() , new 干了几件事?

  • 创建空对象
  • 让新对象继承构造函数的原型对象 设置新对象 proto 指向构造函数prototype
  • 调用构造函数
  • 返回新对象的地址

call() 与 apply()

  • apply(this,arguments)
  • apply 和 call 都是强行借用一个本来无法调用的函数 , 并临时替换函数中的this为指定对象
  • call 必须是单独传参数 , 逗号分隔
  • apply 可自动打散数组类型参数

默认环境下 , this指向全局

var name = 'Bin';
function sayHello(){
    // 默认环境下 , this指向全局
    console.log(this.name);
}
sayHello(); //Bin

var obj = {
    name:'BigBinBin'
}
// 改变了 this , 指向对象
sayHello.apply(obj);    // Bigbinbin

修改this , 指向obj

var name = 'Bin';
// 带参数
function sayHello(a,b){
    // 默认环境下 , this指向全局
    console.log(this.name+'吃了'+(a*b)+'个馒头');
}
sayHello(1,2);

var obj = {
    name:'BigBinBin'
}
// 利用apply带参数
sayHello.apply(obj,[1,5]);
// 利用call带参数
sayHello.call(obj,1,5);         

apply 与 arguments

// 正常数组使用join
var arr = [1,2,3,4,6,8,9,5];
console.log(arr.join('-'));

function foo(){
    // arguments 是一个伪数组
    console.log(arguments);

    // 伪数组没有 join 这个方法
    // arguments.join('-');

    // 借用 数组的 join 方法 , 因为apply 第二个参数要一个数组 
    var str = Array.prototype.join.apply(arguments,['-']);
    return str;
}
var str = foo('1','1','1','1','1');
console.log(str);              

对象this 替换 全局 this

function foo(){
    // 指向全局
    console.log(this);
}
foo();
var obj = {
    name:'bin',
    sayThis:function(){
        console.log(this.name);
    }
};
obj.sayThis();  // -> obj

// 替换this , 同对象的this 替换 全局 this
foo.call(obj);  // -> obj   

全局this 替换 对象this

var name = 'globalName';
function foo(){
    // 指向全局
    console.log(this);
}
foo();
var obj = {
    name:'bin',
    sayThis:function(){
        console.log(this.name);
    }
};
// null是window(全局) , 把null 替换到 obj.name
// 变成 window.name , 打印出 globalName
obj.sayThis.call(null);     

替换构造函数 this

function Person(){
    this.name = 'bin',
    this.age = 21;
}
function Student(){
    // 借用父构造函数 , 把 Student 的this 替换了原来的 Person 的this
    // 继承了person 的属性
    Person.apply(this);
}
var stu = new Student();
console.log(stu); 

两种类型继承 apply

function Flyer(name,speed){
    this.name = name;
    this.speed = speed;
}

Flyer.prototype.fly = function(){
    console.log(`${this.name}以时速${this.speed}飞行`);
}

function Bee(name,speed,award){
    // apply 和 call 都是强行借用一个本来无法调用的函数 , 并临时替换函数中的this为指定对象
    // call 必须是单独传参数 , 逗号分隔
    // Flyer.call(this,name,speed);
    // apply 可自动打散数组类型参数
    Flyer.apply(this,arguments);
    this.award = award
}
Bee.prototype.getAward = function(){
    console.log(`${this.name}以时速${this.speed}飞行,击落获得${this.award}`);
}
Object.setPrototypeOf(Bee.prototype,Flyer.prototype);

var be1 = new Bee('be1',50,'一条命');              
be1.fly();
be1.getAward();

ES5

保护对象

  • Js对象中的属性,随时可以被修改,删除,替换,添加.
  • 命名属性 : 可用.访问到的属性
  • 数据属性:直接存储属性值的属性
  • 每个属性都有四大特征
    • value:实际存储属性值
    • writable:是否可以修改,默认为true
    • enumerable:是否可以被for in 遍历到 , 无法控制 . 访问
    • configurable:是否可修改其他特性 控制 writable , enumerable 以及 是否可删除属性
      • 如果关闭了configurable , 属性将不可删除

获取四大特性

var attrs = Object.getOwnPropertyDescriptor(对象,属性名)

设置四大特效

Object.defineProperty(bin,'id',{

writable:false, // 设置只读

configurable:false // 双保险,禁止再修改 , 不可逆 , 还禁止删除

})

"use strict";
var bin = {
    id:1001,
    name:'Bin',
    salary:10000
};
// 获得四大特性
var attrs = Object.getOwnPropertyDescriptor(bin,'id');
console.log(attrs);
// 修改Id属性为只读 , define定义 , property属性
Object.defineProperty(bin,'id',{
    writable:false, // 设置只读
    configurable:false // 双保险,禁止再修改 , 不可逆 , 还禁止删除
})
// 禁止遍历 , 但是不能控制 . 访问
Object.defineProperty(bin,'salary',{
    enumerable:false,
    configurable:false
})

// 设置为禁止修改 , 修改时候并不报错 , 除非开严格模式
// bin.id++;
// 删除属性
// delete bin.id;   // 设置了 config:flase 报错
console.log(bin);

同时修改多个属性值的四大特性

Object.defineProperties(obj,{

属性名:{特性:值},

属性名:{特性:值}

})

如果要修改的属性不存在会报错吗?

  • 会自动创建新的属性
  • 通过 Object.defineProperty 自动创建的属性 , 四大特性都默认为 false
  • 通过 对象直接添加的新属性, 四大特性为 true
var bin = {
    id:1001,
    name:'Bin'
};
// 同时修改多个属性的四大特性
Object.defineProperties(bin,{
    id:{writable:false,configurable:false},
    name:{configurable:false},
    // salary:{enumerable:false,configurable:false}
    // 通过这个方法添加新属性 , 四大特性默认为 False , 除非手动打开
    salary:{value:1000,writable:true,configurable:true}
})
// delete bin.name;
// bin.id++;
var attrs = Object.getOwnPropertyDescriptor(bin,'salary');
// 通过这个方法添加新属性 , 四大特性默认为 False
console.log(attrs);
console.log(bin);

自定义保护

  • 数据属性四大属性只能做到基本保护 , 不能做到自定义保护
  • 访问器属性:不直接存储属性,专门对其他属性提供保护的特殊属性
var bin = {
    id:1001,
    name:'Bin'
};
(function(){
    // 实际存储数据的变量
    var _age;
    // 保护器属性 age 保护的变量是 _age;
    // 为什么要闭包?
        // 因为外部无法访问 _age , 能访问到的只有 get,set
    Object.defineProperty(bin,'age',{

        get:function(){
            return _age;
        },

        set:function(val){
            console.log(`值${val}接受检查`);
            if(val>=18&&val<=65){
                _age = val;
            }else {
                throw new RangeError("年龄必须介于18~65之间");
            }
            
        }
    })
})();
bin.age=17; 
bin._age=0; 
console.log(bin.age);   
console.log(bin._age);              

// 问题受保护的数据不能保存在普通的数据属性中,因为数据属性可以随便被修改,访问
// 解决: 闭包

在构造函数中使用保护

// 在构造函数中使用四大特性保护
function Emp(id,name,salary,age){

this.id = id;
this.name = name;
this.salary = salary;
var _age;   //实际存储数据的变量
// id只读
// Name禁止删除
// salary禁止遍历,删除
// age要在18-65之间
// this是值正在创建新对象
Object.defineProperties(this,{
    id:{writable:false,configurable:false},
    name:{configurable:false},
    salary:{
        enumerable:false,configurable:false
    },
    // age是访问器属性保护_age
    age:{
        get(){
            return _age;
        },
        set(val){
            if(val>=18 && val<=64){
                _age = val;
            }else {
                throw new Error('年龄少于18或者大于65');
            }
        },
        // age是后天添加的属性全部属性都是false
        enumerable:true
    }
})

this.age = age;
}
// 在实例化对象的时候自带数据保护
var lilei = new Emp(2,'lilei',1000,10);    

保护整个对象

三个级别

  • 防拓展:禁止添加新属性
    • Object.preventExtensions(obj);
  • 密封:及防扩展又禁止删除任何属性
    • Object.seal(obj)
function Emp(id,name,salary,age){
    this.id = id;
    this.name = name;
    this.salary = salary;
    var _age;   //实际存储数据的变量
    Object.defineProperties(this,{
        // 使用了密封 , configurable可以省略
        id:{writable:false},
        salary:{
            enumerable:false
        },
        // age是访问器属性保护_age
        age:{
            get(){
                return _age;
            },
            set(val){
                if(val>=18 && val<=64){
                    _age = val;
                }else {
                    throw new Error('年龄少于18或者大于65');
                }
            },
            // age是后天添加的属性全部属性都是false
            enumerable:true
        }
    })

    this.age = age;

    // 在属性定义完以后,防扩展
        // 防止扩展新属性
    // Object.preventExtensions(this);
    // 密封:及防扩展又禁止删除任何属性
    Object.seal(this); 
 }
 // 在实例化对象的时候自带数据保护
 var lilei = new Emp(2,'lilei',1000,20);

 // Cannot add property abc, object is not extensible
 // lilei.abc = 'abc';  

 // lelie is not defined
 // delete lelie.age;
  • 冻结
    • 防止扩展 禁止删除 同时禁止修改一切属性值
    • Object.freeze(this);
var config= {
    host:'localhost',
    port:'3302',
    db:'test',
    uname:'root',
    upwd:'abc'
}
Object.freeze(config);

兼容 IE

经典继承写法 ES5

var o = {
    name:'Bin',
    age:'21'
};
// 返回一个新对象 , 继承来自参数中的obj
var obj = Object.create(o);
console.log(obj);

IE 8 没有这种写法 , 兼容 IE8

// 兼容IE8
// 为什么有些方法是在原型链 , 有些方法是在构造函数里面?
// 只有某一类才能使用的方法就定义在 这个类型的 原型对象中
    // arr.indexOf();
    // date.indexOf();
    // reg.indexOf();
    // 只要一个方法,只希望本类型的子对象才能调用,就保存在原型对象中
// 不限制类型的方法 , 都写在构造函数中 
// 重要希望所有对象都可以使用的方法
    // Array.isArray(obj);
    // Object.create(father);
if(typeof Object.create!=="function"){
Object.create = function(father,props){
    // 创建新对象
    function F(){};
    F.protptype = father;
    var onj = new F();
    // F函数的作用就是 为了和 father 建立关系 , 然后没用就销毁
    F=null;
    // 继承father
    // Object.setPrototypeOf(obj,father); 
    // 如果有第二个参数,就将第二个参数中的属性舔到新对象中
    if(props){
        // 四大特性是 es5 特有的
        // Object.defineProperties(obj,props);
        // 遍历Props中的每个属性 , 为Obj添加新属性
        for(var key in props){
            obj[key] = props[key.value];
        }
    }
    // 返回新对象


}
}

var father = {
bal:100000000,
car:'bmw'
}
var hmm = Object.create(father,{
// 等于直接调用了 defineProperties
phone:{
    value:'iphoneX',
    writable:true,
    enumerable:true,
    configurable:true
},
bar:{
    value:'C',
    writable:true,
    enumerable:true,
    configurable:true
}
})
console.log(hmm);

bind()

  • 基于一个现有函数,创建一个新的函数,永久绑定this
  • 希望一个对象永久绑定在一个this上
  • 会造成函数中的this将永久无法被其他对象替换
  • 除了永久绑定this ,还可以绑定部分参数
function calc(base,bonus){
    console.log(this.name + '的总工资是'+(base+bonus));
}
var lilei = {
    name:'liLei'
};
var hmm = {
    name:'韩梅梅'
};
// 李雷临时借用calc计算器
    // call有替换this的功能
calc.call(lilei,1000,400);
calc.call(hmm,200,400);
// liLei买了一个和calc一样的计算器,新计算器永久属于李雷
var lilei_calc = calc.bind(lilei);
lilei_calc(10000,400);
lilei_calc(20000,400);
lilei_calc(30000,400);
// 尝试用call抢
lilei_calc.call(hmm,1,2);   // 失败

// 绑定部分参数
var lilei_calc10000 = calc.bind(lilei,10000);
lilei_calc10000(1); // 10001

call apply bind区别

  • call 和 apply 临时借用一个函数,并把this 替换成指定对象
  • call 和 aooly 立刻执行
  • bind 基于现有函数,创建一个新的函数,并永久绑定this为指定对象
  • bind只创建函数 , 并不执行;
  • 如果立刻执行函数使用 call apply
  • 如果只创建一个新的函数就用Bind

其他

语言精粹提出继承的方法

// 语言精粹提出的继承方式
function Jc(obj){
    var o = {};
    // o的原型对象继承obj , obj同时也继承了 Object
    o.__proto__ = obj;
    return o;
}
var o = Jc({name:'Bin'});
console.log(o);

**new Function() **

  • Function 这个构造函数 可以用来创建函数对象
  • 语法
    • 一个参数都不传的情况 创建一个空函数
      • var x = new Function();
    • 只传一个参数的情况 这个参数是函数体
      • var x = new Function('函数体');
    • 传多个参数的情况下 , 最后一个为函数体 , 前面都是形参
      • var x = new Function(x,y,z,'函数体');

eval()

  • eval可以将字符串转换成 js 代码
// 不在严格模式下 , 可以正常转换
var str = 'var a = 10;';

eval(str);

console.log(a);
  • 转换Json
    • 直接转换eval(jsonStr)会出现错误 , 原因是将 {} 转义成代码块而不是对象
  • 解决
    • 在json字符串前面拼接 var o =
    • 把json格式的字符串用()括起来 , 不会讲{}转义成代码块 , 而是表达式
    // JSON
    var jsonStr = '{"name":"Bin","age":"21"}';
    // var jsonObj = eval(jsonStr);
    // console.log(jsonObj);    // SyntaxError 语法错误
    
    // 用eval来解析json字符串的时候,会将{}解析为代码块 , 而不是对象
    // 解决办法
        // 1.在json字符串前面拼接 var o = 
    eval('var jsonObj ='+jsonStr);
    console.log(jsonObj);       // 正常
    
        // 2.把json格式的字符串用()括起来 , 不会讲{}转义成代码块 , 而是表达式
            // 什么是表达式 , 就是运算
    var jsonStr2 = '({"name":"Bin","age":"21"})';   
    var o = eval(jsonStr2);
    console.log(o); 

块级作用域

  • Javascript没有块级作用域
  • Javascript中唯一能产生作用域就是 函数
  • 词法作用域
    • 写好代码那一刻 , 变量的作用域就已经确定了
    • 和词法作用域相对叫做动态作用域 , 但是Js是词法作用域 并不是 动态作用域
  • 词法作用域规则
    • 函数允许访问函数外的数据
    • 整个代码结构中只有函数可以限定作用域
    • 作用域规则首先使用提升规则
    • 如果当前作用域有变量, 就不再向外寻找
    // 写好代码那一刻 , 变量的作用域就已经确定了
    var a = 123;
    function f1(){
        console.log(a);
    }

    function f2(){
        var a = 435;
        // 如果此时是在执行环境调用 a , 那么就是动态作用域 , 而他 直接调用最外层a
        // 所以称为词法作用域
        f1();   // 123
    }
    f1();   // 123
    f2();   // 123  

js执行顺序

  • 线程
    • 一个线程只能处理一件事情 , 多个线程就能同时处理多个事情
    • Js 是单线程
// Alert 时候程序不能继续执行 , 突出Js 是单线程
alert('卡主了,下面代码不能执行');
// 因为alert 不能继续执行
console.log('执行代码');

js执行流程

  • 渲染任务
    • 执行html标签 css
  • Js代码执行任务
    • 平时我们写的代码
  • 事件处理任务
    • 回调

回调往往是执行完主程序 再执行回调函数

setTimeout 异步函数

function foo(){
    for(var i = 0;i<10;i++){
        setTimeout(function(){
            // 输出 10 个 10 , 为什么不输出 0 - 9?
            // 因为这是异步代码 . 执行完for循环 再执行 setTimeout
            console.log(i);
        },0);
    }
}
foo();

console.log('我比setTimeout先执行');
console.log('我也是 , 虽然 设计定时器0秒钟 , 可是他在事件处理 任务中');
console.log('而平时写的代码都在 执行任务环境 中');
console.log('先执行 任务环境中的js代码 , 再去执行 事件处理环境 中的回调');   

图解: 异步代码最后执行

for(var i = 0 ;i<10;i++){
    function foo(j){
        var value = j;
        return function(){
            console.log(value);
        }
    }
  
    var f = foo(i);

    // f(); 这样就是直接执行闭包了;
    
    setTimeout(f,0);

    // setTimeout(f(),0);

    // 这里为什么要用 f , 而不是 f() ?
        // settimeout(function(){},time); 这个是最原始执行 定时器的格式
        // 第一个是参数function(){} , 就是要放一段函数在这里被 setTimeOut 执行
        // foo 返回的是一段函数 , 刚刚好 , 和 setTimeOut 格式一致 .

    
}

// 还是先会执行 主程序代码 , 再执行 setTimeOut 回调
console.log('测试执行顺序');
console.log('测试执行顺序');
console.log('测试执行顺序');
console.log('测试执行顺序');

图解闭包 + setTimeout 图解闭包+ setTimeout

// 假设有10个div
// <div>我的第1个div</div>
// ...

window.onload = function(){
var divs = document.getElementsByTagName('div');

for(var i = 0;i<divs.length;i++){
    var div = divs[i];

    // 变成回调任务 , 等执行完主程序再执行回调 , 这样的话 i就变成10
    // 永远都是 第10个
    // div.onclick = function(){
    //  alert('我的第'+test+'个Div');
    // }

    function foo(i){
        return function(){
            alert('我的第'+(i+1)+'个div');
        }
    }

    var f = foo(i);
    div.onclick = f;
}
}

HTML5

Web Worker

Chrome浏览器的线程模型是怎样的?

  • 程序:计算机可以执行的代码,存在于磁盘中——静止的
  • 进程:把程序调入到内存中,等待被CPU执行——活动的
  • 线程:是CPU执行进程代码的基本单位——生产任务

面试题:进程和线程间的关系?

  • 进程是操作系统分配内存的基本单位。
  • 线程处于进程内部,是CPU执行代码的基本单位。
  • 一个进程中至少有一个线程,也可以有多个。
  • 多个线程间并发执行——宏观上看是“同时”执行,微观上看是“轮流”执行。

Chrome中的线程模型:

  1. Chrome中发起HTTP请求最多可以使用6个并发的线程;
  2. Chrome中负责向页面中执行绘制任务(执行HTML/CSS/JS/事件处理等代码)的只有1个线程——UI主线程。

碰到如下的代码,就有问题了:

<button onclick="console.log(111)">1</button>
<script src="耗时.js"></script>
<button onclick="console.log(222)">2</button>

根本解决之道:

创建一个新的线程,去执行耗时的JS任务——与UI主线程并发执行。new Thread('x.js') —— 其它语言中的类似代码。

HTML5提供了一种类似创建新线程的机制:

new Worker('x.js'); //工人线程,与UI主线程并发执行

注意:Worker线程天然缺陷!!

浏览器禁止Worker线程操作任何的BOM和DOM对象!!!——浏览器只允许UI主线程进行页面内容的渲染!——不能使用Worker加载类似jquery.js文件!! 项目中,必需用到Worker的场景不多,比如有非常复杂耗时的且与DOM无关的运算时。

UI线程与worker通信

UI线程发送消息给 Worker 线程

UI线程:

var message = 'hello';

var worker = new Worker('./x.js');

// 发送给 worker
worker.postMessage(message);

worker:

onmessage = function(message){
    console.log(message);
}

UI线程接受 Worker 线程消息

Worker:

postMessage('worker');

UI主线程

var worker = new Worker('./worker.js');

// 接受 worker 消息 
worker.onmessage = function(message){
    console.log(message)
}

WebSocket

HTTP协议:是基于“请求-响应”模型的协议,客户端发起一个请求,服务器就要返回一个响应,请求消息和响应消息是一一对应的!没有请求就没有响应!

在一些特别的应用场景下(如实时走势图,在线聊天室),只能使用“定时器+AJAX”不停的向服务器发起请求以获得最新的数据——“心跳请求”,解决方案并不完美:心跳过快服务器压力过大,过慢导致数据实时性差。

WebSocket协议:

是基于“广播-收听”模型的协议,只要客户端连接到服务器上,就不再断开,一方可以发送多条消息,对方只接收而不发送,可以解决上述应用中的问题。

这个协议本身的问题:客户端与服务器是“永久连接”,导致服务器可以同时连接的客户端数是有限的!

基于WebSocket协议的应用必需两套程序:

(1)服务器端程序:

可使用PHP、Java、Node.js等语言编写

注意:PHP编写的WebSocket服务器独立运行的,无需依赖Apache!

(2)客户端程序:

使用HTML5提供的WebSocket对象创建WS客户端:

<body>
    <button id="btConnect">连接到服务器</button>
    <button id="btSendReceive">发送消息</button>
    <button id="btClose">关闭服务器</button>
    
    <script>
        
        var wsClient = null;

        btConnect.onclick = function(){
            wsClient = new WebSocket('ws://127.0.0.1:9999');
            wsClient.onopen = function(){
                console.log('客户端已经练到服务器');

            }
        }

        btSendReceive.onclick = function(){
            wsClient.send('hello'); // 向服务器发送消息
            wsClient.onmessage = function(e){
                console.log('客户端接收到服务器消息');
                console.log(e.data)
            }
        }

        btClose.onclcik = function(){
            wsClient.close();
            wsClient.onclose = function(){
                console.log('ws已经断开连接')
            }
        }

    </script>

</body>