番外篇:PHP开发者的JavaScript快速文档

本文提供word格式文档,下载链接: https://pan.baidu.com/s/1CcM2Dl4_rG0qe0omxPG8zQ 提取码: 9isx

前言:

每一位PHP开发者或多或少的都掌握一些JavaScript知识,本文写在《云客Drupal8源码分析》前端相关章节发布前,意在帮助沉浸在后端世界的phper快速进入前端js世界(推荐phper关注学习Drupal,那是php世界的珠峰,极其强大灵活的cms系统或者框架,被称web操作系统,非常优秀),但本文是独立的js教程,写作首要原则是全面,但每个知识点简明,将庞大的js知识压缩为一篇简明教程,如果需要js详细资料推荐阅读著名犀牛书《JavaScript权威指南》,可惜目前(20195月)该书尚未发布包含ES6的第七版。主要面向php开发者介绍目前常用的ES5.1版和ES62012年时所有现代浏览器均已支持ES5.1),足以支撑阅读代码和常用js操作,但并不介绍服务器端js相关内容,为方便记忆本文将对比和PHP语言的异同,更多参考资料在文尾列出。

 

概述:

js的语法标准由ECMA国际制定,官方地址为:http://www.ecma-international.org,该组织前身为:欧洲计算机制造商协会(European Computer Manufacturers Association),为体现开放、中立的国际组织性质,后改名为“ECMA国际”,因此该名称已不属于首字母缩写,ECMA国际维护很多标准,其中262号系列标准即为js语法标准,标准是随着时间发展的,262号标准包含了js已发布标准的所有版本,有个较重要的时间点:20156月,该月发布了《ECMAScript 2015 标准》,改动较大,目的是为让js支持大型软件开发,如服务器端脚本,此后每年六月发布新的小幅改进的标准,因此习惯上将20156及以后的标准统称为ES6,记住ES6是一个泛指,并不是具体某年发布的特定版本,ES2016ES2017相当于ES6.1ES6.2等等,所有js标准的版本见:

http://www.ecma-international.org/publications/standards/Ecma-262-arch…

目前有不少工具可以将ES6标准的js转码为之前的版本标准。js不仅可以在浏览器中运行,也可以在服务器端像php脚本一样运行,甚至可以在桌面环境运行(见Rhino项目,可调用java的全部API),在不同环境运行有不同组成部分,但核心规范相同,如浏览器环境下由js核心、DOMBOM组成,在服务器环境下,安装运行环境“node.js”即可,这里“node.js”是一个js的运行平台,类似php引擎,并不是服务器软件,“node.js”封装了谷歌V8引擎来执行js脚本;服务器端js运行时,无需专门的服务器软件,因此需要js脚本自行侦听端口,自行完成服务器功能,底层调用由“node.js”提供。

 

符号:

操作符基本和PHP一样,以下列出一些不一样或需要注意的符号:

加号“+ ”:

表示两个数相加或者连接两个字符串,在php中连接字符串是点号“.”;js加号的连接操作优先于相加运算,换句话说字符串和数字相加将是把数字转换成字符串再连接

引号:

php中双引号会检测内部是否有变量,而js不管什么引号都不会检查变量,字符串在反引号中才能嵌入变量,推荐用单引号,她们区别并不大:双引号里面的字符串会经过编译器解释,然后再当作HTML代码输出;而单引号里面的不进行解释,直接输出

和号“&”:

和号在php中能使基本类型变量按引用传递,在js中无此用法,但都用来做位运算符

测试属性是否存在“in”:

示例:var yunke={a:1,b:2};if('b' in yunke){console.log('存在');},注意调试js往往用console.log();向浏览器控制台输出数据,本文将大量运用该方法

删除运算符“delete”:

类似phpunset(),删除可配置的对象属性或数组元素(见变量的可配置性),删除后为undefined(该值见后),删除数组元素后并不改变数组长度(length属性值),删除后可用in操作符测试

void

对任何值返回undefined,如:<a href="javascript:void 'yunke';">Click me</a>,点击后没有任何动作

箭头函数“=>”:

箭头“=>”在php中用于数组,但ES6js用来简写函数定义,见下

类型运算符“typeof”:

用在变量或字面值前面,以返回类型,如:console.log(typeof "me");,返回类型有:undefinedboolean numberstringfunctionobject,其中object代表引用类型或 Null 类型(在jsnull作为对象的占位符)

判断是否实例“instanceof”:

typeof判断类型时可能会有点问题,可用该运算符补充,如:

        var a="yunke";
        var b= new String("yunke");
        console.log(typeof a); //向浏览器调试控制台输出string,按F12查看
        console.log(typeof b); //object
        console.log(a instanceof String);//false
        console.log(b instanceof String);//true

该运算符和原型链有关系,原型链见后

无符号向右移位“>>>”:

php没有该运算符,可用自定义函数代替

扩展运算符“”:

这是在ES6js新增的,叫扩展运算符,用在数组前面,将其展开形成逗号分隔的参数,后面可接表达式;通常用于函数调用,如:console.log(...[1, 2, 3])相当于console.log(1, 2, 3),除了调用也可以用于函数定义:

    function push(array, ...items) {
        array.push(...items);
    }
    var arr = [1, 2];
    push(arr, ...[3,4]);//注意不能省略“...”

php5.6版本及以上也有该运算符,叫做可变参数运算符,作用在以上情况时是类似的,但js中有更多功能,常见如下:

1、数组克隆与合并:

    const arr1 = ['a', 'b'];
    const arr2 = ['c'];
    const arr3 = ['d', 'e'];
    // ES5 的合并数组
    var arr = arr1.concat(arr2, arr3);
    // [ 'a', 'b', 'c', 'd', 'e' ]
    // ES6 的合并数组
    var arr = [...arr1, ...arr2, ...arr3];

2、与解构结合

3、将字符串转变为真正的数组(这可通过数组的.length属性判断字符长度,能正确识别非双字节的Unicode 字符,见下)

4、转化实现了 Iterator 接口的对象为数组(实际上扩展运算符背后就是调用遍历器接口)

ES2018中将扩展运算符用到了对象上:

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1
    y // 2
    z // { a: 3, b: 4 }

解构赋值时左侧扩展运算符只能放在最后一个参数上,以接收全部剩余可遍历属性,执行的是浅拷贝,仅列举可遍历属性,原型不被复制;在变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式,其他用法和数组类似

以下操作符和php中的含义完全相同:

全等“===”、不全等“!==”、相等“==”、不等“!=”、大于“>”、大于等于“>=”、小于“<”、小于等于“<=”、且“&&”、或“||”、非“!”、自加“++”、自减“--”、求模“%

指数运算“**”(又叫幂运算、指数运算,是ES6新增的)

算数运算符“+-*/%”,均可与等号连用(如a+=b;

位运算符:且“&”、或“|”、互斥(xor)^”、向左移位“<<”、有符号向右移位“>>”、取补数“~

三目运算“(condition)?value1:value2

逗号“,”从左到右连续执行表达式,如for (i = 0; i < 10; i++, j++){}

 

注释:

PHP一样,多行使用/*内容*/  单行注释://

 

语句:

语句末尾的分号是可选的,如省略将以行末作为语句的结束,如果在一行中有多条语句,则语句间分号是必须的, php与此不同,其要求语句必有分号,和php一样JavaScript 会忽略多余的空格,因此可以使用空格格式化代码

关于换行您可以在文本字符串内部使用反斜杠对代码进行折行,如:

document.write("Hello \
World!")

但是不能像这样折行:

document.write \
("Hello World!")

 

保留关键字:

ES5.1关键词、保留词:

breakcasecatchcontinuedefaultdeletedoelsefinallyforfunctionifininstanceofnewreturnswitchthisthrowtrytypeofvarvoidwhilewithabstractbooleanbytecharclassconstdebuggerdoubleenumexportextendsfinalfloatgotoimplementsimportintinterfacelongnativepackageprivateprotectedpublicshortstaticsupersynchronizedthrowstransientvolatile

ES6新增关键词、保留词:letasyncawait

 

大小写:

JS变量名、常量名、函数名、类名均区分大小写,连布尔字面量、null、关键词都必须小写

PHP变量名、常量名区分大小写,函数名、类名、布尔字面量、null、关键词不区分大小写。

 

变量:

js变量名必须以字母、下划线或$开始,不能使用保留关键字。

声明变量:

var a;  var a=5;  var a=1, b=2;

这里varvariable(变量)的缩写,和php一样都是弱类型语言,无需声明数据类型;在相同作用域中再次通过var声明变量时,其值不会被重置或清除,除非声明的同时赋值;在全局作用域中通过var声明的变量无法用delete删除;在不同作用域中通过var声明变量代表声明不同的变量(即使变量名相同也是不一样的变量),不论在哪个作用域中,如果向未声明的变量赋值将在顶级作用域(全局作用域)隐式自动声明该变量。

在一个作用域中声明变量时,声明会被隐式提前到该作用域的开始,这样一来,在该作用域中声明的变量始终可见,这称为声明提前,是在预编译阶段执行的,见:

    var yunke = 'yunke';
    function test() {
        console.log(yunke); //输出undefined,而不是全局变量中的值,因为进行了声明提前
        var yunke;
        yunke = 'me';
        console.log(yunke); //输出me
    }
    test();

取用未经声明的变量会报错并终止脚本执行(typeof操作除外),这一点和php不同

 

动态变量名:

php允许动态变量名:

$a="yunke";
$$a=5;
echo $yunke;

js无法像php那样直接支持,需如下:

    var yunke={a:'yunke',b:'is',c:'phper'};
    var varName='b';
    console.log(yunke[varName]);

将输出“is”,这里varName不能加引号,也不能采用点号方式

 

变量作用域:

js的作用域分全局作用域和函数作用域,函数作用域是局部的,里面的变量只能在函数内使用,由于函数内部还可以声明函数,内部声明的函数可用函数内变量,因此变量形成了作用域链,全局作用域是作用域链中的第一个节点,最外层的函数是第二个节点,依次到最里层的函数,最里层的函数作为作用域链的最后一个节点,在作用域链中,前面作用域中的变量在其所有后面的作用域中可见可用,反之则不行,换句话说,在外层作用域的变量自动在里层作用域可用,反之则不行,这点和PHP不一样,php需要用global关键词引进,js则直接使用;在使用变量时,依据作用域链向全局作用域方向依次查找,里层可以通过var声明同名变量以屏蔽外层变量;函数退出运行后没有被返回引用的变量会被注销,全局变量的生存期从声明它们开始,到页面关闭时结束;见代码:

<script type="text/javascript">
    var x = 0;
    function fun1() {
        var x = 1;
        function fun2() {
            var x = 2;
            function fun3() {
                console.log(x); //此处x值为2,用作用域链中找到的第一个变量x,最近作用域的值
            }
            fun3();
        }
        fun2();
    }
    fun1();
</script>

php中的函数是超全局的,只要被定义随处可使用,但js函数的作用域和变量相同,函数名可视为代表该函数的变量名,在上例中如果在fun1();后面执行fun3();将出错,因为在作用域链中找不到该函数,作用域链对理解闭包和with语句很重要(见后)。

ES6中新增了let关键词来声明变量,和var用法类似,但有以下区别:

1let声明的变量是块级作用域,一个“块”作用域即一个花括号,仅在块及块的内层子作用域中可用,这和var的函数作用域是很不一样的,通常用在循环中,示例如下:

    {
        let a = '块级变量';
        var b = '函数级变量';
    }
    console.log(a);//提示ReferenceError: a is not defined
    console.log(b);//正常输出

2、没有声明提前,必须在声明后使用,如提前使用将报错,这样也不行:

    var tmp = 123;
    if (true) {
        tmp = 'abc'; // 报错
        let tmp;
    }

3let不允许在相同作用域内重复声明相同变量,这样也不行:

    function fun1() {
        let a = 1;
        var a = 2;
    }

4、在块作用域中使用let时必须有花括号,if (true) let x = 1;将出错,而应为:if (true) {let x = 1;}

5let声明的全局变量,不再是window对象的属性:

    let a="yunke";
    console.log(window.a);//输出undefined

 

常量:

ES6中新增关键词const来声明常量,如:const yunke = '我是云客';,重复声明或赋值常量将报错,作用域和let声明的变量相同,都是块级作用域,都不会声明提前;通过const声明的常量不是window对象的属性;当将一个对象赋值给常量时,常量保存的是这个对象的引用,常量意为保证引用不变,但对象是可以改变的,如:

    const foo = {};
    foo.prop = 123;// 为 foo 添加一个属性,可以成功
    console.log(foo);
    foo = {};//指向另外一个对象,将出错

 

数据类型:

js的数据类型分原始类型和引用类型,原始类型有五种:UndefinedNullBooleanNumber String。其他都是引用类型,又叫对象类型,部分类型介绍下:

undefined 类型:

该类型只有一个值,即undefined

var yunke;
console.log(typeof yunke); //输出undefined
console.log( undefined === yunke);//输出true

注意比较时没有加引号,声明后未赋值、无返回值的函数将全等该值,值 undefined 实际上是从值 null 派生来的,在条件判断时都相当于false,和null的区别是:

console.log( undefined==null);相等而不全等,undefinedundefined类型,而nullobject类型

含义上undefined 是声明了变量但未对其初始化时赋予该值,null 则用于表示尚未存在的对象

null是关键词,而undefined是一个预定义的全局变量

js中只有nullundefined无法拥有方法和属性,在php中不存在undefined

 

数字:

js只有一种数字类型,不存在整数、浮点数一说,采用IEEE 754标准64位浮点格式储存,可以使用小数点,这可以表示很大的数(正负10308次方),能够准确表示的整数范围在-2^532^53之间(不含两个端点),极大或极小值用科学计数法表示,如5e2500)、5e-10.5),这里e后面跟一个整数意为10的多少次方,支持二进制(前缀为0b)、8进制(前缀为0o)和16进制(前缀为0x),前缀可大写,尽管所有整数都可以表示为二、八或十六进制的字面量,但所有数学运算返回的都是十进制结果。

有个特殊的常量: NaN,表示非数(Not a Number),用于在类型(StringBoolean 等)转换失败时,例如,要把单词 blue 转换成数值就会失败,因为没有与之等价的数值,与无穷大一样,NaN 也不能用于算术计算,NaN 有个奇特之处是它与自身不相等,console.log(NaN == NaN);输出 false,用函数isNaN()来判断是否为非数字,isNaN("666")isNaN(666)均返回false

数字的Num.toString(2)方法可以返回不同进制表示,不传递参数默认为10进制,常用进制有2/8/16

 

字符:

js采用utf-16编码的Unicode字符集,只有字符串类型,没有字符类型,字符串是由无符号的16位值组成的序列,最常用的Unicode字符都是通过16位内码表示,字符串长度是16位的个数(也就是有多少个双字节),如:

var str="云客";  console.log(str.length);//输出2

这将输出2,但并不是所有Unicode字符都是两字节,比如“𝑒”(不是字母eUnicode字符码为“\ud835\udc52”)使用了4字节,尽管是一个字符但其length将输出2,因为占用了两个16位组,类似的字符还有很多,比如中文“𠮷”,详见Unicode编码规则,要正确得到字符串长度可用扩展运算符:[...str].length,该运算符是ES6添加的。

字符串赋值时可采用字面值方式,也可以采用Unicode字符码方式,如:

  var str="\ud835\udc52";  console.log(str);

但该方式只能表示\u0000~\uFFFF之间的字符,所以在ES6中可用大括号方式来表示超过此范围的字符,如:

var s = "\u{20BB7}";//输出“𠮷”

当有大段字符串时,ES6可以使用反引号``Esc下面那个键)包裹,里面可用${}来嵌入变量:

    var s = "yunke"; 
    var str=`我是${s},js用反引号嵌入大段字符串,里面的空格、换行等都会被保留`;

这称为标签模板,在反引号表示的字符串前可加一个函数名做标签,表示要用该函数来处理她,函数返回值作为表达式的值,如:alert`123` 等同于alert(123),当无嵌入变量时,字符串直接做函数的参数,当有嵌入变量时,会先将字符串处理成多个参数再调用函数(第一个参数是嵌入变量分隔的数组,后面参数是依次传递的嵌入变量值,${}中可以用任意表达式,传入的是其表达式值),注意:${}不用于单双引号,js没有提供在单双引号中嵌入变量的功能;

js中字符串是只读的,可被当做数组,可以使用str[0]方式表示其中的字符,但以这种方式赋值是无效的(不抛出错误)

 

日期:

日期类型可以比较,如:

    var myDate = new Date();
    myDate.setFullYear(2008, 8, 9);
    var today = new Date();
    if (myDate > today) {
        console.log("今天在2008-9-9之前");
    }
    else {
        console.log("今天在2008-9-9之后");
    }

 

ES6新增的Symbol类型:

ES5中只有5种原始类型和一种对象类型,Symbol类型是ES6新增的第七种类型,用来提供一个独一无二的标志,通常用于保证对象属性的唯一性,类似UUID,使用起来类似储存了一个UUID的变量,但该变量的类型是专用的symbol

    let mySymbol = Symbol('yunke');
    console.log(typeof mySymbol); //输出“symbol”
    let a = {[mySymbol]: 'Hello!'};
    console.log(a[mySymbol]);

这里函数Symbol('yunke');返回一个symbol标志,其参数用于描述这个标志,相同参数调用多次时,尽管描述相同但每次返回的symbol都是不一样的,如果需要一样的可用Symbol.for('yunke');来返回上次用这个描述时返回的结果,symbol标志做属性时,只能用不带引号的方括号方式,不能用在点号后面,否则将被当做标识符而不是symbol标志,这表现的像变量名一样;函数Symbol();前不能使用new命令,否则报错。这是因为生成的 Symbol 是一个原始类型的值,类似字符串,不是对象;Symbol 值不能与其他类型的值进行运算,比如+连接等,可以显式转为字符串、布尔值,但是不能转为数值

Symbol 作为对象的属性名,该属性不会出现在for/infor/of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回,但它也不是私有属性,可用Object.getOwnPropertySymbols方法获取指定对象的所有 Symbol 属性名,要获得所有类型的键名,包括常规属性和 Symbol 属性可以用Reflect.ownKeys方法,除了自定义使用Symbol 值以外,ES6 定义了 11 个内置的 Symbol 值,指向语言内部使用的方法:

Symbol.hasInstance属性:

foo instanceof Foo运算将调用方法Foo[Symbol.hasInstance](foo)

Symbol.isConcatSpreadable属性:

布尔值,表示对象用于Array.prototype.concat()时,是否可以展开

Symbol.species属性:

指向一个构造函数。创建衍生对象时,会使用该属性

Symbol.match属性:

指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值

Symbol.replace属性:

指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值

Symbol.search属性:

指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值

Symbol.split属性:

指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值

Symbol.iterator属性:

指向该对象的默认遍历器方法

Symbol.toPrimitive属性:

指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值

Symbol.toStringTag属性:

指向一个方法。调用对象toString方法时,如果该属性存在,其返回值会出现在返回的字符串之中

Symbol.unscopables属性:

指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除

 

类型转换:

类型转换通常使用转换函数Boolean(value) Number(value)String(value),这和php的(intvalue方式不同,对象转化为原始类型时较复杂,通常是调用对象上的一个方法,转化为字符串用toString(),转化为数字用valueof(),所有对象转化为布尔将是true,即便是代表false的布尔对象也将转化为true,条件比较时特别要注意这一点,有些操作符也会进行隐式转换,如:+、!等

 

函数:

普通方式(声明方式):

<script type="text/javascript">
function name()
{
alert("Hello World!")
}
</script>

普通方式也叫函数声明方式,其定义的函数可以先调用后定义,也就是所谓的声明提前,提前时如果函数内用了在调用处尚未初始化的变量,则变量值为undefinedjs函数可重新声明,此时以重新声明的为准,这一点和php不同,更不同的是函数其实是一个对象,它本身可以拥有属性和方法,还可以被当做参数传递:

    function yunke() {
    }
    yunke.a=88;
    yunke.f=function(){this.a=66;};
    yunke.f();
    console.log(yunke);
    console.log(yunke.a);

所有函数都是Function的实例:

    var yunke = new Function("iNum", "console.log(iNum + 10)");//最后一个参数做函数体,前面的做函数参数
    var php = yunke; //同一个函数可以有很多函数名
    yunke(10);	//输出 "20"
    php(10);	//输出 "20"

虽然函数是对象但typeof操作符返回function而不是object

 

表达式方式:

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

这种方式定义的函数只是变量名被声明提前,函数体并没有;表达式方式的函数名是可选的,存在时在递归操作中很有用,如:

var yunke = function yk(num) {return num<1?yk(num-1):console.log('递归减1成功');};

此时函数名是函数体内的一个局部变量,此列中yk仅函数体中有定义,如果是声明方式(非表达式方式)时,函数名就不仅在函数体内有效,只要在该函数的作用域中都有效。

 

函数可以匿名定义并立即运行:

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

JS大小写敏感,关键词function如果大写就会出错,函数名同样大小写敏感,函数内部还可以声明函数,但内部声明的函数是局部的,外部不可使用,这一点和php不一样,php函数是超全局的;不要在if语句或循环语句中采用声明方式声明函数(表达式方式不受限制),虽然有些实现支持,但这样做代码移植性不好。

函数内部可以使用关键词this,分情况含义不同,当函数挂载到对象上,就是对象的方法,其内部的this是执行上下文,此时指向被挂载的对象,一个函数可以被挂载到不同对象上运行,ECMAScript 的第三版为 Function 对象加入了两个方法:call() apply(),通过它们可以让函数被临时挂载到对象上充当对象的方法来执行,函数里面的thiscall() apply()的第一个参数相同,如:

    function sayColor(sPrefix,sSuffix) {
        console.log(sPrefix + this.color + sSuffix);
    }
    var objA = new Object();
    objA.color = "blue";
    sayColor.call(objA, "颜色是", "!");//第一个参数是被挂载对象,后面依次传递函数参数
    sayColor.apply(objA, new Array("颜色是", "!")); //以数组方式传递函数参数
    var objB= new Object();
    objB.color = "red";
    sayColor.call(objB, "颜色是", "!");
    sayColor.apply(objB, new Array("颜色是", "!"));

函数如果没有被挂载到任何对象上执行,则在非严格模式下this指向全局对象Window,严格模式下为undefined,如果被当做构造函数使用则this指向正在实例化的对象,如果方法被当做构造函数使用也是指向正在实例化的对象,换句话说:new o.m();mthis并不指向o;注意this不是变量,而是一个关键词,不允许被赋值,也不被内部嵌套的函数继承。

函数内执行return 语句后立即停止并返回,return 之后的代码不被执行,如果函数没有调用return return 无参数将返回undefined

js不会验证传递给函数的参数个数是否和定义的相等,开发者定义的函数都可以接受任意个数的参数(最多可接受 255 个),按顺序赋值给形参,没被传递的参数会采用默认值,没有默认值则用undefined 做值,多余的将忽略,这一点和php不一样。在不定参数时,php可以用func_get_args();获取函数参数信息, js使用特殊变量arguments来获取实参,是一个数组,第0个元素是第一个参数,arguments.length代表参数个数。

前文说了函数是一个对象,它的length属性为默认期望的参数个数,在ES6中可以给函数参数赋默认值,此时length属性会失真,不包括有默认值的参数;函数对象的toString()方法可以显示函数源代码

js中函数参数不存在类型暗示,不像php可以指定参数类型;js函数也没有静态变量,因为函数是对象,所以当需要用到静态变量功能时可以用函数的属性代替:

    function yunke() {
        if (yunke.staticVar === undefined) {
            yunke.staticVar = 1;//这里使用函数名,而不是this
        } else {
            yunke.staticVar++;
        }
    }

或者:

    var yk=function yunke() {
        if (yunke.staticVar === undefined) {
            yunke.staticVar = 1;//这里使用函数名,而不是this
        } else {
            yunke.staticVar++;
        }
        return yunke.staticVar;
    }
    console.log(yk());

函数的作用域是在定义时决定的,而不是调用时,这是理解闭包的必须前提,闭包通常是指返回的在内部作用域中定义的函数,她可带出内部作用域中的变量,闭包技术功能强大,运用很广泛,但闭包技术太灵活,运用时需要小心,见以下代码:

    function counter() {
        var n = 0;
        return {
            count: function () {return ++n;},
            reset: function () {return n = 0;},
        }
    }
    var a = counter(), b = counter();
    console.log(a.count());//输出1
    console.log(b.count());//输出1 因为第二次调用counter()时是不一样的n
    a.reset();
    console.log(a.count());//输出1
    console.log(b.count());//输出2 两次调用b的count()方法是同一个n

 

箭头函数:

ES6 新增了使用“箭头”(=>)定义函数,这是定义函数的一种简写方式:

    var f = v => v;

等同于:

    var f = function (v) {
        return v;
        };

箭头“=>”左边是参数,右边是函数体,有多个参数或函数体有多条语句时可以这样写:

var sum = (num1, num2) => { num1+=1; num2+=1;return num1 + num2; };

当没有参数时左边可以用空括号,参数也是可以有默认值的,此时不要误会是给外部变量赋值

箭头函数和普通函数有些区别,有几个需要注意的地方:

1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

2)不可以当作构造函数,换句话说,不可以使用new命令,否则会抛出一个错误。

3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用扩展运算符解决(见解构)。

4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数(生成器函数见下文)。

 

语句结构:

条件语句:

        if (yunke < 50) {
            console.log("小于50");
        } else if (50 <= yunke <= 100) {
            console.log("50和100之间");
        } else {
            console.log("大于100");
        }

注意多分支if判断当中的“else  if”之间有空格,而在PHP中没有,实际上js的“else  if”是多条if语句的组合简写,js大小写敏感如果写成IF会出错,如果if语句只有一行代码可以省略花括号:“if(i>3) break;

 

多分支语句:

switch(n)
{
case 1:
  执行代码块 1
  break;
case 2:
  执行代码块 2
  break;
default:
  n 与 case 1 和 case 2 不同时执行的代码
}

注意:在jsswitch语句的比较操作采用全等比较“===”,而PHP使用松散比较

for循环:

    var yunke = "yunke";
    for (var i = 0; i < yunke.length; i++) {
        console.log(yunke[i]);
}

 

for( in )循环:

用于遍历数组或者对象的可枚举属性,该结构在PHP中没有(类似的用foreach语句代替):

    var mycars=["Saab","Volvo","BMW"];
    var i;
    for(yunke in mycars){
        console.log(mycars[yunke]);
  }

in关键词后面的应该是一个对象或数组(如果是原始类型则转化为包装对象),前面的变量是键名或属性名,不可枚举的属性或方法不会被遍历,如语言核心定义的内置方法是不可枚举的,用户定义的对象的所有属性和方法都是可枚举的。在phpforeach语句中是遍历一个副本,而jsfor/in如果在循环体内删除了还未遍历的属性时,被删除的属性将不被遍历,如果是添加则各实现可能不同,通常也不会遍历;遍历通常是按照属性的定义顺序进行,但规范中并没有明确规定,在ES6中引入了for/of循环来遍历值,见下文的遍历器。

 

while循环:

while (条件)
  {
  需要执行的代码
  }

 

do/ while循环:

do
  {
  需要执行的代码
  }
while (条件);

 

循环跳出:

不管是以上哪种循环,循环体内均可用“break”语句结束循环,结束本次循环进入下一次可用“continue”,在php中支持“continue”加数字以跳出多层循环,js不支持该功能,但可以使用标签语法,为语句或语句块加标签的形式如下:

label : statement”,如:“start : i = 5;”,

标签必须是合法的标识符,这里标签“start”可以被之后的 break continue 语句引用,如:

        var a = 0;
        yunke:
            for (var i = 0; i < 10; i++) {
                for (var j = 0; j < 10; j++) {
                    if (i == 5 && j == 5) {
                        continue yunke;
                    }
                    a++;
                }
            }
        console.log(a);	//输出 "95"

标签仅在它作用的语句内起作用,可以定义多个标签,每个标签不能和其起作用的语句内部的标签重名,换句话说就是不能和内部嵌套的标签重名,不嵌套时可以同名

 

with语句:

with(obj){
  //该块里面的变量作用域得到了扩展      
}

该语句用于临时扩展作用域,在作用域链的头部临时挂载一个新的作用域以解析变量,执行完花括号即恢复,比如要赋值一个表单值时:

document.forms[0].email.value="";

当要大量使用表单里面的属性时,都需要加document.forms[0]前缀会比较麻烦,可以用with简化:

    with (document.forms[0]) {
        email.value = "";
    }

这样在使用email时就会首先在document.forms[0]下查找,进而简写了变量名;在with中如果传入的变量没有某属性,但花括号里面新建了该属性,并不会为传入对象新建,而是和没有使用with相同

不推荐使用with语句,因为难于优化且运行缓慢,在严格模式下该语句是禁止的

 

严格模式:

在代码顶部或函数体顶部使用“'use strict'”表示全局代码或函数内代码使用js严格模式,如:

    function yunke() {
        'use strict'
        console.log('本函数内代码以严格模式运行');
    }

'use strict'”是ES5规范引入的,但之前已经被很多浏览器实现了,这并不是语句而是指令,放置在脚本或函数的实体语句之前,但并不是一定要在第一行;ES6 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

严格模式中有许多限制,如:不能使用with语句,所有变量都需要先声明,函数(非方法)调用中的this值不同,给只读属性赋值或给不可扩展对象新建属性将抛出异常,eval函数属于不同,不能使用八进制字面量等等

 

错误处理:

    try
    {
        //在此运行代码,通过throw抛出错误或语法、js引擎发生错误
        var a=yunke;//取用没有定义的变量 因此抛出错误
    }
    catch(err)
    {
        //在此处理错误
        //注意err很特殊,在这里具备块级作用域,仅在catch中有效
        console.log('变量yunke未定义')
    }finally {
        //这里的代码主要起清理作用,不管是否有异常总会执行
        //catch和finally都是可选的
    }

可以在try中自定义抛出的错误:“throw exception;”其中“exception”可以是字符串、数字、逻辑值或对象,如:

    try {
        var x = "yunke";
        if (isNaN(x)) throw "not a number";
    }
    catch (err) {
        console.log("Error: " + err + ".");
}

 

对象:

JavaScript 是面向对象的编程语言 (OOP),但在ES5.1中并没有class这个概念,生成实例对象的传统方法是通过构造函数,构造函数就是一个普通的函数,只不过是用来构造对象而已:

    function person(firstname, lastname, age, eyecolor) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.age = age;
        this.eyecolor = eyecolor;
        this.changeName = function (name) {
            this.lastname = name;
        };
    }
    var myFather = new person("Bill", "Gates", 56, "blue");
    var myMother = new person("Steve", "Jobs", 48, "green");
    console.log(myMother.lastname);
    myMother.changeName('Kally');
    console.log(myMother.lastname);

这里new操作符用于对象创建表达式,内部过程是js先建立一个空对象并调用一个函数,将函数内的this指向这个空对象,由函数来初始化这个空对象,这函数就是构造函数,它不需要显式返回值,默认返回this,如果返回了值(期待返回对象,返回原始值或undefined不算有返回值,此时依然返回this),则该对象创建表达式的值为返回的值,开始新建的对象(this)就废弃了,记住这一点很重要;在php中对象是class实例化而来,class是对象的模板,但ES5js并无固定模板(class),全凭构造函数,类似的,构造函数本身的方法或属性就相当于php类的静态方法或属性,是属于类的,被实例共享,构造函数中this的方法或属性就是普通的方法或属性,是属于实例的,每个实例不共享;在ES6中增加了类class概念,但其并不是脱离历史表现的像php的类那样,而是基于ES5等历史形成的一层新语法,类似语法快捷方式,见后文(有C++java经验的程序员可能觉得php的类才是一种不违和的做法,但js基于历史有一条自己的发展道路,不可能跨越式发展而不顾应用上的兼容)。

 

js对象是属性的无序集合,方法也称为属性(有时说到属性时也包括方法),访问对象的属性和方法有两种方式:

1、用点号连接:“对象名.属性名”、“对象名.方法名()”,这和php的“->”不同

2、用方括号:“对象名[属性名]”、“对象名[方法名] ()”,在php中这是数组的用法,但js中这是对象的用法,当不知道属性名或属性名不是合法标识符时,就需要使用方括号方式,此时属性名可以用表达式计算或用任意字面量表示,js的对象属性名可以是包含空字符串在内的任意字符串

 

php一样可在对象实例化后动态添加新的属性;JavaScript中一切皆可视为对象,注意“可视为”和“均是”是不一样的,原始类型(字符、数值、布尔)不是对象但均可被当做对象使用,此时将隐式临时实例化一个包装对象,包装对象使用完后即销毁,请看如下代码:

    var str = "yunke";
    str.a = 4; //被当做对象使用,自动创建包装对象,赋值后随即销毁
    var t1 = str.a; //再次自动创建包装对象,但实际不存在属性a
    console.log(t1); //因此输出undefined
    var s = new String("yunke");//显式采用对象方式
    s.a = 5; //并非临时创建的包装对象
    var t2 = s.a;//属性a是有值的
    console.log(t2);//输出5

由该代码可见自动转化和显式声明的区别,对原始类型的属性赋值(临时对象上的赋值)会自动忽略。

js的数组和函数也属于对象,这对phper的认知是有颠覆性的,如:

    var person = function () {console.log("函数对象");};
    person.yunke = "我是函数对象的属性";
    person();//执行函数对象
    console.log(person);

任意对象均是Object的后代,当您声明一个变量时,可以使用直接实例化对象方式:

person=new Object();
person.firstname="Bill";

或使用字面量定义方式:

person={firstname:"John",lastname:"Doe",age:50,eyecolor:"blue"};

注意属性名没有引号,但属性名如果是非合法标识符则必须用引号,单双引号均可

 

jsphp中都有自动回收机制清理不再使用的对象释放内存,对象都是以引用传递,变量实际上保存的是引用,但js的比较操作是比较其引用是否相同,这意味着即便两个有相同属性和值的变量只要不是同一个引用也是不相等的:

    var yunke1={a:1};
    var yunke2={a:1};

表达式“yunke1==yunke2”为false,更加不全等,由于js数组属于对象(见后),比较操作和此相同。在php中比较操作是不同的,只要属性和值全相同就认为相等:

$a=new yunke();
$b=new yunke();

以上php代码$a$b相等而不全等

js对象的属性或方法全是公有的,不存在privateprotected,也不存在静态static方法或属性,但都可被模拟,js不支持clone关键词,js的克隆涉及多个方面,除了属性和值外,还要考虑对象及其属性的特性(见下)。

变量this用在方法中,总是指向调用该方法的对象,如果是全局方法将指向Window对象,如:

    function showColor() {
        console.log(this.color);
    };
    var oCar1 = new Object;
    oCar1.color = "red";
    oCar1.showColor = showColor;
    var oCar2 = new Object;
    oCar2.color = "blue";
    oCar2.showColor = showColor;
    oCar1.showColor();		//输出 "red"
    oCar2.showColor();		//输出 "blue"

 

js对象分三大类:内置对象(js规范定义的对象)、宿主对象(如浏览器定义的对象)、自定义对象(用户代码定义的对象);JavaScript 拥有若干内置的对象,比如 Number StringDateArray、函数等等。

创建数字对象var myNum=new Number(1);隐式包装对象:var myNum=1;

创建字符串对象var myString=new String("yunke");隐式包装对象:var myString= "yunke";

创建布尔对象var myBoolean=new Boolean(true); 隐式包装对象:var myBoolean=true;,参数为0-0null""falseundefined 或者 NaN时为false,其他为true(即便为字符串方式的’false’

当保存原始数字、字符串、布尔值的变量被当做对象使用时,将用以上对象隐式包装一个新对象使用

创建日期对象var myDate=new Date();//这会赋值当前日期

创建正则表达式对象

var reg= new RegExp("正则表达式",修饰符); 

或字面量方式(创建后类型为对象):

var reg= /pattern/修饰符;

修饰符只有三个:gglobal全局查找)、iignoreCase忽略大小写)、mmultiline多行查找)

ES6中增加了几个修饰符:

u:含义为“Unicode 模式”,用来正确处理大于\uFFFF Unicode 字符

y:类似g,但g只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始

s:使得点号.可以匹配任意单个字符,包括四个字节的 UTF-16 字符和换行

RegExp 对象有 3 个方法:test()exec() 以及 compile()

reg.test(“被检索的字符串”);

依据是否找到返回truefalse

reg.exec(“被检索的字符串”);

返回值是被找到的值,如果没有匹配,则返回 null

reg.compile(regexp,modifier);

重新设置检索用的正则表达式和修饰符

算术对象“Math:算术对象不用创建,是已经存在的内置对象,被当做命名空间使用,其方法和属性用于提供算术运算和数学常量,无需实例化,直接使用属性或方法,如:

  console.log(Math.PI);//圆周率
  Math.round(-4.40);//四舍五入 值为-4

 

js的对象同样可以序列化,如:

    var yunke = {a:1,b:2};
    var str=JSON.stringify(yunke);
    var obg=JSON.parse(str);

JSONjs对象表示法:JavaScript Object Notation)来表示序列化后的对象,jsonjs的对象或数组字面量写法很接近,但有差别(可将json视为子集),比如不能表示函数对象、NaN和正负无穷被表示为null、日期字符串不被还原为日期对象等等。JSON只能序列化对象可枚举的自有属性,对于不能被序列化的属性将省略,更多json内容可查看:http://json.org/json-zh.html

 

对象的特性:

每个对象除了包含属性外,还有三个对象特性:

类:

phpclass是不同的概念,仅是一个标识对象类型的字符串,可用如下代码显示:

console.log(Object.prototype.toString.call(obg).slice(8,-1));

但很遗憾所有自定义构造函数创建的对象的类属性均是“Object”,因此用处不大,类型对象可以通过以上代码显示出类属性,如:

console.log(Object.prototype.toString.call(new Date()).slice(8,-1));

原型:

每一个js对象(null除外)都和一个相对于它叫做“原型prototype”的对象相关联,原型用于继承目的,换句话说对象会继承其原型对象上的属性和方法,在大多数浏览器中对象用 __proto__ 属性指向原型对象,在对象创建之前原型属性就设置好了,原型还可以有原型,直到Object.prototype,形成了一个原型链,原型类似父类,js中所有对象均是Object.prototype的子类,在原型上定义的新属性和方法,将实时应用到所有子对象,所谓“新”是指实例化对象后定义的,关于对象Object本身有以下几点注意:

    console.log(Object.prototype.__proto__===null);//输出true
    console.log(Object.__proto__===Function.prototype);//所有对象是函数实例化来的,因此输出true
    console.log(Function.prototype.__proto__===Object.prototype);//输出true
    console.log(Object.__proto__.__proto__===Object.prototype);//输出true

对象的原型是可以被改变的,获取或设置一个对象的原型:

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

这里为什么不直接用“yunke.__proto__”呢?因为ES6标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的,且在早期有些浏览器并不支持该属性,但现在几乎所有主流浏览器都支持了(包括IE11),赋值该属性将改变对象的原型,“yunke.__proto__”是全等于“Object.getPrototypeOf(yunke)”的,注意对象的原型并不是其“prototype 属性,该属性用于函数,见后。

判断一个对象是否是另一个对象的原型:

    var yunke = {a: 1, b: 2};
    var obg=Object.create(yunke);
    console.log(yunke.isPrototypeOf(obg));//返回true,yunke是obg的原型

因为对象Object.prototype是几乎所有对象的原型,她的方法可被所有对象使用,有些著名方法使用较广:

    obg.toString();//返回对象的字符串表示,当用+时,自动调用该方法,类似php的__toString()
    obg.toLocaleString();//返回对象的本地化字符表示,在时间对象上很有用
    obg.valueOf();//返回对象包裹的原始类型值,数值对象上很有用

扩展标记:

指明是否可以向对象添加新属性,所有内置对象和自定义对象默认是可扩展的(可被转化为不可扩展),宿主对象的可扩展性由js引擎定义,判断和设置如下:

    var yunke = new Number(1);
    console.log(Object.isExtensible(yunke)); //判断是否可扩展
    Object.preventExtensions(yunke); //将对象转化为不可扩展的
    console.log(Object.isExtensible(yunke));

一旦将对象转化为不可扩展后,将无法再次变为可扩展的,可扩展性仅仅影响对象本身,在原型上添加的属性依然可以被继承,以下几个函数还可以进一步锁定对象:

    var obg = new Number(1);
    Object.seal(obg);//封闭对象,不但转为不可扩展,还将对象自有属性转为不可配置
    Object.isSealed(obg);//检测对象是否封闭
    Object.freeze(obg);//冻结对象,在封闭的基础上将自有属性设为只读
    Object.isFrozen(obg);//检测对象是否冻结

以上函数都返回传入的对象,但不必显式接收返回值,因为对象是以引用方式传递的

 

对象的属性:

存取器属性:

对象除了普通属性外,还有存取器属性,这用于实现类似php__set($name, $value)__get($name)魔术方法功能,见:

    var yunke = {
        name: "存取器属性示例",
        _b: 4, //定义一个辅助用的属性,用下划线表明应被当做私有使用
        get random() {
            return Math.floor(Math.random() * Math.pow(10, this._b));
        },
        set random(n) {
            if (n >= 1 && n <= 12) {
                this._b = n
            }
        }
    }
    console.log(yunke.random);
    yunke.random = 4;
    console.log(yunke.random);

在这个示例中yunke.random就是存取器属性,用getset声明,每次读取都不一样,返回一个随机数,赋值yunke.random相当于指定随机数的最大位数,在php中为不存在的属性统一设置get/set魔术方法,而js为每一个存取器属性设置get/set

 

属性的特性(属性元数据)

对象的属性除了名字和值外,在ES5中增加了三个属性特性:

可写:是否可以赋值该属性

可枚举:是否可以通过for/in语句遍历

可配置:是否可以删除或修改该属性

ES5之前这些特性均是可以,这些属性用描述符对象表示,查看描述符对象:

    var yunke = {a:1,b:2};
    var descriptor=Object.getOwnPropertyDescriptor(yunke,'a');
    console.log(descriptor);

描述符对象有四个属性:configurableenumerablewritablevalue分别代表可配置、可枚举、可写、值,前三个为布尔,value是属性值,对于存取器属性来说描述符对象没有writablevalue属性,而是用setget代替,其值是函数,可以使用以下方法为对象属性设置描述符对象,也就是配置属性的特性:

    var yunke = {a: 1, b: 2};
    Object.defineProperty(yunke, 'a', {value: 3, writable: true, enumerable: true, configurable: true});
    console.log(yunke.a);

或者使用Object.defineProperties()方法批量设置,在设置时描述符对象的属性时不需要设置的特性可省略

 

属性读取、赋值、删除:

对象的属性分为自有属性和从原型链上继承的属性,在取值时首先查找对象自有属性,如果没有则依次查找原型链,如果最终没有找到就返回undefined,并不报错,但查找不存在的对象的属性将报错;在赋值时仅查找对象自有属性,如果没有即新建(如果原型链中有,将被新建的属性屏蔽),不会影响原型链,但如果对象本身或原型链表明该属性是只读的,则不允许新建或赋值(但在非严格模式下并不报错),Object.prototype就是只读的;delete只能删除自有属性,不能删除继承属性,也不能删除可配置性为false的属性(此删除在非严格模式下不会报错)

 

遍历对象:

只有可枚举的属性才能被遍历,for/in将遍历自有和继承的可枚举属性,对象继承的内置方法均不可枚举,在用户代码中给对象添加的属性默认可枚举,方法Object.keys(Obg)返回一个数组,包括Obg所有可枚举的自有属性名称,方法Object.getOwnPropertyNames(Obg)返回数组,包含Obg所有自有属性的名称,不仅仅是可枚举的。

 

属性检测:

可以用以下几种方式:

if(x in obg){} //使用in运算符
Obg.hasOwnProperty("x") //返回布尔,仅检测是否存在自有属性
Obg.propertyIsEnumerable('x') //返回布尔,仅检测自有且可枚举的属性
使用Obg.x!==undefined可检测自有和继承的属性

 

数组:

js中数组是一个特殊的对象,而不像在php中那样和对象属于两种类型,js并没有数组类型,这对phper的认知是颠覆的,对象是属性的无序集合,数组是对象的子集,表现为属性是有序集合、可遍历、元素以数字做属性名(对象的属性是可以采用非法标识符的),但为了强调其特殊性,通常叫做数组,而不称为对象,因为数组是对象,因此在函数调用中也是传递的引用,如果在函数内部被修改,外部会跟着变化;为什么要数组呢?因为索引查找会很快;声明或创建一个不指定长度的数组(长度为数组元素的个数):

var arrayObj = new Array();

声明或创建一个指定长度的数组(这里参数并非表示仅创建一个元素并赋值该元素):

var arrayObj = new Array(3);

声明或创建一个带有默认值的数组:

var arrayObj = new Array("abc",1,2,3,true);

创建一个数组并赋值的字面量简写

var arrayObj = ["abc",1,2,3];

数组的长度用数组对象的length属性表示:

    var mycars=new Array()
    mycars[0]="Saab"
    mycars[2]="BMW"
    console.log(mycars.length);

此时mycars.length3,变量mycars[1]的值为undefined,像这种有空缺的数组称为稀疏数组,长度比所有下标都大(这里并没有说比最大索引值大1,针对非稀疏数组才可以这样讲),设置length属性将截断或加长数组

为数组赋值:

var mycars=new Array()
mycars[0]=" Volvo"
mycars[1]="BMW"

如果此时出现:mycars['yunke'] = 'phper';,这是合法的,但并不是将数组当做php那样的关联数组处理,而是因为数组是对象,只是给数组对象添加了一个属性,而不是一个元素,该列中数组长度依然为2,长度不包含属性个数,在js中类似php的关联数组(非数字键名)以对象方式存在,如:

var person={firstname:"Bill", lastname:"Gates", id:5566};

因为js仅有索引数组,元素的索引是非负整数(任何非负整数以外的属性名都将被当做数组对象的属性看待而非索引),数字不是合法的标识符,因此访问元素不能采用点号方式,只能使用方括号方式,方括号内索引数字有无加引号效果一样,给索引数字加引号并不表示其为一个属性,依然是元素;和php一样,数组元素可以是不同的类型,索引用32位数值表示,最大有(232次方减2个)元素。

js中有很多数据表现的像一个数组,称为类数组,比如DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象等,对类数组运用某些数组相关的方法会导致错误,有时错误还不会提示,字符串可被当做数组来使用,也属于类数组,可使用Array.isArray(arr);来判断传入参数是否为真正的数组,在ES6中添加了将类数组转化为真正数组的方法:Array.from()

以下列出数组操作的部分方法:

arr.push('a','b');//向数组末尾添加一个或多个元素
arr.unshift('x','y');//向数组首部添加一个或多个元素
delete arr[1];//删除一个元素,不影响其他索引和数组长度
arr.pop();//从末尾删除并返回一个元素,长度减1
arr.shift();//从头部删除并返回一个元素,长度减1,所有索引依次降1
arr.join('');//返回一个以参数连接数组而成的字符串,无参数默认为逗号
arr.split('');//是join的逆向操作
arr.reverse();//颠倒元素顺序,直接作用在arr上
arr.sort();//字母序排序数组,直接作用在arr上
arr.slice(0,2);//依据位置截取数组并返回
arr.splice(0,2);//删除或插入元素的通用方法,直接作用在arr上,很简单不详解
arr.forEach(f);//用函数遍历数组,不关心返回值,无法终止遍历,除非抛出异常
arr.map(f);//用函数遍历数组,返回经过函数处理的新数组,原数组不受影响
arr.filter(f);//返回函数过滤后的数组,过滤函数应该返回布尔

 

Set Map

ES6新增了数据结构 Set,类似于数组,但其成员值都是唯一的,没有重复的值:

    var yunke = new Set([1, 2, 3, 4, 4]);
    yunke.add(5);
    console.log(yunke.size);//输出5

Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化;加入值不进行类型转换,使用全等判断是否重复,但NaN等于自身,Set的实例有以下方法:

add(value):添加某个值,返回 Set 结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值
values():返回值的遍历器,遍历以插入顺序进行
keys():返回键名的遍历器,由于set没有键名,因此和values()完全一样
entries():返回键值对的遍历器,名值相同
forEach():使用回调函数遍历每个成员

同时提供了WeakSet 结构,与 Set 类似,但成员只能是对象,而不能是其他类型的值,且对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,即便在里面有保存也可能被回收,没有遍历操作,也没有size属性。

 

js对象中属性名都是字符串,ES6 新增了 Map 数据结构,提供值到值之间的映射,类似对象,但属性名可以是任意类型值(此时不应称为属性名,叫做键名更适合):

    var yunke = new Map();
    var obj = {p: 'Hello World'};
    yunke.set(obj, 'content')
    console.log(yunke.get(obj));

在键名是对象时,只在是同一个对象的引用,Map 结构才将其视为同一个键,其他值时需要全等,但NaN被认为和自己相等,Map实例的size属性表示元素个数,此外具备有以下方法:

keys():返回键名的遍历器。遍历时按插入顺序
values():返回键值的遍历器。
entries():返回所有成员的遍历器。
forEach():遍历 Map 的所有成员。
set(key, value):添加某个值,返回对象本身
get(key):取回某元素
has(key):判断某个键是否在当前对象之中
delete(key):删除某个键,返回true。如果删除失败,返回false
clear():清除所有成员,没有返回值

Map 转为数组时会形成一个二维数组,如:

    var yunke = new Map();
    yunke.set('a', 'Hello');
    yunke.set('b', 'World');
    var arr =[...yunke]
    console.log(arr);//输出:[['a', 'Hello'],['b', 'World']]

相应的也提供了WeakMap结构,其与Map结构类似,也是用于生成键值对的集合,但只接受对象作为键名,且键名所指对象是弱引用,可能被垃圾回收机制销毁,没有遍历操作,也没有size属性

 

遍历器(Iterator):

ES6 中用于表示数据集合的数据结构有数组、对象、MapSet,为了统一遍历操作ES6新增了遍历器Iterator概念,其作用有三个:一是为各种数据结构提供统一、简便的访问接口;二是使得数据结构的成员能够按某种次序遍历;三是 ES6 创造了一种新的遍历语法for/ of循环,Iterator 接口主要供for/of使用。

一个对象只要实现了属性Symbol.iterator(见Symbol数据类型)即被认为是实现了遍历接口,是可遍历的,属性Symbol.iterator的值是一个方法,称为遍历器生成器,调用它将返回一个遍历器对象,调用遍历器对象的next方法将返回一个只有两个属性的简单对象,称为遍历结果对象:

 { value: value, done: false }

属性value的值为当前遍历得到的值,属性done是一个布尔值,指示遍历是否已经到达了末尾,注意遍历器生成器、遍历器、遍历结果对象之间的区别(她们也用在下一节的生成器中)。只要实现了遍历接口就可以用for/of结构遍历,如:

    var data = {};
    data[Symbol.iterator] = function () {
        var nextIndex = 0;
        var arr = [1, 3, 5, 7, 9];
        return {
            next: function () {
                return nextIndex < arr.length ?
                    {value: arr[nextIndex++], done: false} :
                    {value: undefined, done: true};
            }
        };
    }
    for (var value of data) {
        console.log(value);//将依次输出1, 3, 5, 7, 9
    }

如你所见,在该列中data是一个空对象,但定义了Symbol.iterator属性因此是可遍历的,遍历输出完全取决于定义的遍历器。有如下原生具备 Iterator 接口的数据结构:

    ArrayMapSetStringTypedArray、函数的 arguments 对象、NodeList 对象

默认普通对象不可遍历(因为无法确定属性的顺序),如要for/of遍历须实现遍历接口(定义Symbol.iterator属性,原型链上的对象具有该属性也可),以上数据结构的默认遍历都可被开发者修改,如果不用在for/of结构中,遍历器可单独使用,如:

    let iter =[2,4,6][Symbol.iterator]();
    console.log(iter.next());//返回了遍历结果对象{done: false,value: 2}

在解构赋值、扩展运算符、生成器、字符串等地方都会用到遍历器,遍历器对象除了必须的next方法外,还可选的实现以下方法:

return方法:提前退出(出错或有break语句),就会调用return方法

throw方法:主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法

注意遍历map对象时,结果对象返回的值是数组,因此可以这样用:

    for (let [key, value] of map) {//内部使用了解构赋值
        console.log(key + ' : ' + value);
    }

 

生成器Generator

生成器函数是遍历器生成函数的变体,它在语言层面创造了一种新的结构:分步执行函数,示例如下:

    function* helloWorldGenerator() {
        yield 'hello';
        yield 'world';
        return 'end';
    }
    var yunke = helloWorldGenerator();
    for (let i = 0; i < 4; i++) {
        console.log(yunke.next().value); //依次输出hello world end undefined
    }

生成器函数声明时类似普通函数,但在关键词function和函数名之间添加了一个*号(在php中可以在函数前加&表示按引用返回,但和这含义完全不同),添加位置ES6规范没有做限制,很多浏览器以下4种方式均可:

    function* f(){}
    function * f(){}
    function *f(){}
    function*f(){}

但通常使用第一种方式,函数体里面的yield关键词后可以是任意表达式,表示当执行到此时暂停并返回表达式的值,调用生成器函数时,她并没有开始执行,而是返回一个遍历器对象(上列中的yunke变量),当调用这个遍历器对象的next()方法时生成器函数才开始真正执行,yield把函数分成了多个部分,每调用一次next()就执行一个部分,从而实现了按yield关键词分隔的步骤分步执行;yield语句只能用于生成器函数,但不是必须的,一个也没有时生成器函数只起到暂缓执行的作用;yield语句如果嵌入其他表达式时需要加括号,她可以放在赋值表达式右侧,默认返回undefined,但可以返回遍历器的next()方法传递的参数,next()在开始下一步的执行前可以带一个参数,将其传递给上一步暂停的yield语句来赋值,这为注入外部值提供了方法,示例:

    function* yunke() {
        yield 'a';
        var x=yield 'b';
        yield x;
        return 'd';
    }
    var g = yunke();
    console.log(g.next().value); //输出a
    console.log(g.next().value); //输出b,输出后还没有开始给x赋值
    console.log(g.next('c').value); //传递'c'给x(相当于为接下来的步骤提供一个初始值),并输出c
    console.log(g.next().value); //输出d

如你所见,当用g.next()时函数内的return也会像yield一样返回值,但使用for/of循环则不被输出,这是因为for/of循环是先判断遍历结果对象的done值,如果是true将不再关注value值;

生成器返回的遍历器除了next()方法还有以下两个方法:

throw方法:

用于在外部抛出异常,示例:

    function* yunke() {
        try {
            yield 'a';
            yield 'b';
        } catch (e) {
            console.log('内部捕获', e);
        }finally {
            console.log('c');
        }
        yield 'd';
        yield 'e';
    }
    var g = yunke();
    console.log(g.next().value); //输出a
    g.throw('外部信息'); //抛出的错误被生成器内部捕获,输出“内部捕获 外部信息”,继续输出c
    console.log(g.next().value); //不会输出d,而是e,因为抛出异常会附带执行一次next()
    console.log(g.next().value); //输出undefined

在外部抛出的异常将会在生成器内部捕获(前提是至少执行过一次next方法),如果不捕获将抛出到生成器函数的外部,一旦抛出异常将结束生成器函数正在运行的try部分,并附带执行一次next方法,其他部分的yield还可以继续执行,注意遍历器的throw方法和关键词throw是无关的,互不影响

return方法:

返回给定的值,并且结束遍历生成器函数,示例:

    function* yunke() {
        yield 'a';
        yield 'b';
        yield 'c';
    }
    var g = yunke();
    console.log(g.next().value); //输出a
    console.log(g.return('ok').value); //结束生成器,并将参数当做本次遍历的值,输出ok
    console.log(g.next().value); //输出undefined

 

以上三个方法本质上都是替换生成器的yield表达式并恢复执行:

next()yield表达式替换成一个值

throw()yield表达式替换成一个throw语句

return()yield表达式替换成一个return语句

yield*”语句:

如果生成器嵌套了另外一个生成器,并需要把内层的生成器展开到外层,可以使用“yield*”语句,示例:

    function* f1() {
        yield 'a';
        yield 'b';
        return 'c';
    }
    function* f2() {
        yield 'x';
        yield* f1();
        yield 'y';
    }
    for (let v of f2()){
        console.log(v); 
    }

以上代码将依次输出xaby,注意没有c,内层return返回的值将作为外层yield*语句的返回值,如:

let r=yield* f1();

此时r的值为'c'

 

因为生成器函数就是遍历器生成函数,因此可把她赋值给对象的Symbol.iterator属性,从而使对象可遍历(具有 Iterator 接口),严格意义讲生成器函数并不能算是函数,它不能跟new命令一起用,也就是说不能作为构造函数,返回是总是遍历器对象,但ES6 规定返回的遍历器是生成器函数的实例,换句话说她继承了生成器函数的prototype对象上的属性和方法。

 

对象继承:

原型链方式:

原型是js的继承机制,对象将自动继承其原型对象上的属性和方法,在通过new构造函数的方式实例化对象时,构造函数的prototype属性将自动作为新实例化的对象的原型,换句话说:

    var obj=new F();
    obj.__proto__ 全等于 F.prototype

属性prototype是函数默认就有的,在非函数对象中默认不存在,但可以被建立。继承见代码:


    function ClassA() {
        this.nameA = "green";
        this.sayNameA = function () {
            console.log(this.nameA);
        };
    }
    ClassA.prototype.color = "blue";
    ClassA.prototype.sayColor = function () {
        console.log(this.color);
    };

    function ClassB() {
        this.nameB = "red";
        this.sayNameB = function () {
            console.log(this.nameB);
        };
    }
    ClassB.prototype = new ClassA();//关键代码,此处是“new ClassA();”而不是“ClassA;”
    var a=new ClassB;
    a.sayNameA();
    a.sayNameB();
    a.sayColor();

这个列子实现了两级继承,ClassB的实例继承自ClassA的实例(ClassB实例化的对象的原型是ClassA实例化的对象),ClassA的实例又继承自ClassA.prototypeClassA实例化出的对象的原型是ClassA.prototype),原型对象构成了一个链,称为原型链,由对象的“__proto__”属性串接,调用对象的属性或方法时,依次在原型链中查找,就形成了继承链,这种继承是动态的,换句话说就是在原型上添加的方法或属性,将实时反映到子对象上可用,这是因为函数的prototype属性和对象的__proto__属性都是保存的同一个对象的引用,注意如果函数的prototype属性被替换成了别的对象的引用(如对象字面值赋值),此时和__proto__将不是相同引用,这导致新添加的属性不会被之前初始化的对象继承;原型链方式不支持多重继承(和php一样是单根多级的继承方式,也可以叫做单重多层),自然支持instanceof运算符

if(a instanceof ClassA){
        console.log("a是ClassA的实例");
}

实际上instanceof运算符就是依据原型属性来判断的

 

函数方式:

原型链方式的简单实现是使用Object.create()函数,它返回一个对象,以传入的第一个参数做返回对象的原型对象,上面的代码可实现如下:

    var ClassA = Object.create({color: "blue", sayColor: function () {console.log(this.color);}});
    ClassA.nameA = "green";
    ClassA.sayNameA = function () {console.log(this.nameA);};
    var ClassB = Object.create(ClassA);
    ClassB.nameB = "red";
    ClassB.sayNameB = function () {console.log(this.nameB);};
    var a = ClassB;
    a.sayNameA();
    a.sayNameB();
    a.sayColor();

注意这里ClassAClassB不是构造函数了,而是对象,不能执行a instanceof ClassA

 

对象冒充方式:

    function ClassA(sColor) {
        this.color = sColor;
        this.sayColor = function () {
            console.log(this.color);
        };
    }
    function ClassB(sColor, sName) {
        this.newMethod = ClassA;
        this.newMethod(sColor);
        delete this.newMethod;

        this.name = sName;
        this.sayName = function () {
            console.log(this.name);
        };
    }
    var objA = new ClassA("blue");
    var objB = new ClassB("red", "John");
    objA.sayColor();	//输出 "blue"
    objB.sayColor();	//输出 "red"
    objB.sayName();

这里ClassB继承了ClassA(它们都仅是构造函数而已,但有时将js构造函数等同于类),关键代码是使 ClassA构造函数成为 ClassBthis的方法,然后调用它,在实例化时,ClassB中的this其实是新建的正在实例化的对象,借助ClassA来构造这个对象,这样ClassB就拥了ClassA的所有属性和方法,此方式叫做对象冒充,本质是将其他构造函数挂载到正在实例化的对象上执行,这种继承方式可以实现多重继承,也就是同时继承多个父对象,有多个根(php语言无法进行多重继承,多重和多层是不一样的,php是单根多层继承,多重是多根的意思),多重继承时如果出现同名属性或方法,那么后面的覆盖前面的。

尽管实现了继承功能,但严格来说这不算是js正统的继承方式,只是利用了构造函数的构造功能,其不支持instanceof运算符也说明了这一点,在上例中:“objB instanceof ClassB”将是true,但“objB instanceof ClassA”将是false,而原型链方式严格支持instanceof运算符。

 

原型与构造函数:

当且仅当继承自同一个原型对象时,对象才是同一个类的实例,不同构造函数的prototype如果指向相同原型,那么其构造的对象属于同一类的实例,同一个构造函数实例化出的对象不一定是同一类,因为构造函数的prototype可以被改变,instanceof运算符即是依据原型进行判断,以下代码两次判定将不一样:


    function ClassB() {
        this.nameB = "red";
        this.sayNameB = function () {
            console.log(this.nameB);
        };
    }
    var a=new ClassB;
    if(a instanceof ClassB){
        console.log("a是ClassB的实例");
    }else{
        console.log("a不是ClassB的实例");
    }
    ClassB.prototype = {};
    if(a instanceof ClassB){
        console.log("a是ClassB的实例");
    }else{
        console.log("a不是ClassB的实例");
    }

js中任意函数都可以作为构造函数,为此给每一个函数都准备了原型prototype属性,该属性有一个constructor属性来指向本函数,换句话说,对任意函数F,都有F.prototype.constructor全等于F,因此构造函数实例化出来的对象O,其O. constructor没被赋值时通常指向它的构造函数,反过来F.prototype.constructor如果被重新赋值,那么所有F实例化的对象的constructor属性都指向新值,除非其被赋值过。

 

ES6新增的class:

ES6中增加了类概念,但和php的类很不一样,可以将js的类视为从ES5发展出来的语法快捷方式,和函数类似都是一个对象,绝大部分功能ES5 都可以做到,声明示例如下:


    class A {
        //a0 = "a0"; //部分浏览器可以这样声明属性
        constructor(va) {
            this.a1 = 'a1'; //声明属性的标准方式
            this.v = va;
            console.log('class A 的构造函数已运行,实例化完成');
        }
        af1() {
            console.log('class A 的普通方法af1');
        }
        ['af2']() {
            console.log('class A 的普通方法af2,以表达式计算方法名');
        }
        get m() {
            return 'class A 的存取器方法取回';
        }
        set m(value) {
            console.log('class A 的存取器方法设置: ' + value);
        }
        *aGenerator() {
            yield 'yunke';
            console.log('class A 的生成器方法');
        }
        static afs() {
            console.log('class A 的静态方法');
        }
    }

    let obj_a = new A();
    obj_a.af1(); //调用对象的普通方法
    obj_a.af2();
    A.afs(); //调用类的静态方法
    A.yunke="给类赋值静态属性";
   var x=A; //类是一个对象,可以赋值给其他变量

 

如你所见,所有方法前均没有“function”关键词;以“constructor”方法做构造函数,不存在析构函数;属性在构造函数中以this动态添加,部分浏览器支持在类体顶部声明属性,比如chrome,但目前还不通用,火狐、微软edge均不支持;可用get/set设置存取器属性;可用“*”在方法名前声明生成器方法;可用“static”声明静态方法,静态方法只能在类上调用,实例上不存在,因此静态方法可以和普通方法同名;方法名可以是表达式;不存在私有、保护的属性或方法,都是公有的。

php中类是声明一种结构,仅用于实例化对象,而在js中类和函数一样被当做一个对象,可以用来赋值或被赋值,类的静态方法是直接定义在类上的(静态属性可以在类声明后动态添加,如上例的A.yunke),除静态方法外,所有其他方法均是定义在类的prototype属性上,上例中:obj_a.af1 A.prototype.af1是全等的,类实例的所有方法在类的prototype属性上查找,换句话说对象的原型即是类的prototype属性,这和从构造函数实例化对象是统一的,类中构造函数的prototype和类的prototype全等,因此说js中的类是在ES5基础上发展出来的,有以下全等关系:

obj_a.constructor.prototype === A.prototype
obj_a.constructor === A
A.prototype.constructor === A
obj_a.constructor === A.prototype.constructor
obj_a.__proto__ === A.prototype
obj_a.__proto__ === obj_a.constructor.prototype

类名相当于其内构造函数的别名,执行console.log(typeof A)将输出“function”,可见class的数据类型就是函数,类只是构造函数换一种语法而已,但类实例化时必须和new关键词结合,不能当做函数直接调用;

类内可以省略构造函数,此时JavaScript 引擎会自动添加一个空构造函数,构造函数默认返回this,但可以显式返回其他对象,此时实例化出的就是其他对象,这和ES5构造函数行为一致

类实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(类的prototype属性上)。同样,在类的prototype属性上或实例的__proto__属性上添加方法将运用到所有实例上

 

前文讲过类和函数一样都属于对象,因此可以被用于表达式:

var MyClass = class yunke{};

此时类名“yunke”可以省略,如不省略也只能在类体里面使用,外面无定义。和函数一样可以写出立即使用的类,如:

    new class {
        echo(){console.log('一次性类')}
    }().echo();

 

ES5中构造函数prototype属性上的方法是可枚举的,而类的prototype属性上的方法不可枚举,也就是说类内部所定义的方法是不可枚举的。

类和模块的内部,默认就是严格模式,就只有严格模式可用

类声明不存在声明提前,必须先定义后调用,且不能重复声明

ES6引入了new.target属性,一般用在构造函数之中,返回new命令作用的那个构造函数,如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的,在类体调用new.target,返回当前 Class,当前是指当子类继承父类时,new.target返回子类,利用该特点可以写出抽象类(不能独立使用、必须继承后才能实例化的类)。

 

class继承:

类可以通过extends关键字实现继承(和ES5一样,ES6的类依然是单根多层继承),接上一节的列子:


    class B extends A {
        constructor() {
            console.log('子类B构造函数运行开始');
            super();//执行父类构造方法
            console.log('子类B构造函数运行完毕')
        }
        bf1(){
            super.af1();//普通方法中super指A.prototype
            super.x="普通方法中赋值时super被当做this";
            console.log(super.x); //输出undefined
            console.log(this.x);
        }
        static bfs() {
            super.y="静态方法中赋值时super被当做当前子类";
            console.log(super.y);//相当于A.y,输出undefined
            console.log(B.y);
        }
    }
    var obj_b = new B();
    obj_b.af1();
    B.afs();
    console.log(B.yunke)
    obj_b.bf1();
    B.bfs();

在子类构造函数中必须调用super();且必须在子类使用this前调用,她表示执行父类构造函数,如果子类省略构造函数那么js引擎会自动调用她,super()返回值是子类B的实例,即全等于thissuper()内部的this指的是B的实例,内部过程是:先产生子类的初始实例,然后让父类先去构造,子类对其的构造必须位于父类之后;super()只能用在子类的构造函数中,但如果super被当做对象使用时,可用在任意方法中,但其含义要分几种情况:

在静态方法中:通过super调用父类的方法时,指父类(A,而不是A.prototype),在被调用的父类方法中this指向当前的子类,而不是子类的实例,但是向super的属性赋值时,如super.x=1;,则super被当做当前子类。

在普通方法中:通过super调用父类的方法时,其指父类的原型(A.prototype),注意不是父类的实例,引用不到A中通过this定义在实例上的属性,被调用方法内部的this指向当前的子类实例,但是向super的属性赋值时,如super.x=1;,则super等同于this,指向当前的子类实例。

 

父类的静态方法和属性可被子类继承,注意不是子类的实例对象继承,因此要以类名引用,换句话说,父类是子类的原型,子类实例的原型是父类的实例,以下表达式皆为真:

    console.log(B.__proto__ === A)
    console.log(B.prototype instanceof A)
    console.log(B.prototype.__proto__ === A.prototype)
    console.log(obj_b.__proto__ instanceof A)
    console.log(obj_b.__proto__ === B.prototype)

其中B.prototype并不等于new A();;在B.prototype并不存在A中通过this声明的属性,但B的实例却有,其是通过super()方法获得,类是按以下模式实现继承:

Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);

关键字extends允许从构造函数继承,换句话说后面可以是一个函数,其prototype属性会被子类继承:

    A = function () {};
    A.prototype.yunke=function () {
        console.log("类继承构造函数");
    }
    class B extends A {
    }
    var obj_b = new B();
    obj_b.yunke();

这意味着可以自定义类去继承原生构造函数以实现自定义的数据结构,有如下原生构造函数:

Boolean()Number()String()Array()Date()Function()RegExp(Error()Object()

 

对象在ES6中的改变:

允许在字面量中使用变量或方法时进行省略属性名简写,如:

    var foo = 'bar';
    var baz = {foo,yunke(){return "Hello!";}};

相当于:

 var baz = {foo: 'bar',yunke:function(){return "Hello!";}};

这种写法用于函数返回值将非常方便:

    function getPoint() {
        const x = 1;
        const y = 10;
        return {x, y};
    }

以字面量方式定义对象时,ES6允许使用表达式作为属性名,须将表达式放入方括号,如:

let propKey = 'foo';
let obj = {[propKey]: true, ['a' + 'bc']: 123};

逻辑不通时,属性名表达式不能和简写方式一起用,如const foo = 'bar';const baz = { [foo] };,以下方式则可以:

var obj = { ['yun' + 'ke']() {return 'hi'; } };

简写的方法中可以使用关键词super指向当前对象的原型,但有注意事项:

    var proto = {
        foo: 'hello'
    };
    var obj = {
        foo: 'world',
        yunke() {
            //console.log(super===this.__proto__);//不能这样用,抛出错误
            //console.log(super);//不能这样用,抛出错误invalid use of keyword 'super'
            console.log(super.foo);//输出'hello'
        }
    };
    obj.__proto__=proto;
    obj.yunke();

可用Object.is(foo1, foo2)判断两个对象是否全等,和全等不同之处是+0不等于-0,二是NaN等于自身

可用Object.assign(target, source1, source2);合并多个对象到第一个对象,仅复制可遍历属性,如有同名,则后面的覆盖前面的,执行浅拷贝,会执行存取器属性

 

解构赋值

ES6中增加了变量的解构赋值,类似php的“list($var1,$var2)=[2,3];”,但js的解构赋值非常强大,可以对数组或对象进行解构赋值。

数组示例:

最简单的情况:let [a, b, c] = [1, 2, 3];

可以使用任意嵌套结构:let [foo, [[bar], baz]] = [1, [[2], 3]];

可以带默认值:let [x, y = 'b'] = ['a'];

对象示例:

完整写法:let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

变量名和属性名相同时可简写:let { foo, bar } = { foo: 'aaa', bar: 'bbb' };

当属性名和要使用的变量名不同时可以这样写:

let { foo: f, bar: b } = { foo: 'aaa', bar: 'bbb' };

此时fb分别等于'aaa' 'bbb',而foo bar仅作为解构模式名,并没有声明他们做为变量

对象解构也可带默认值:var {x: y = 3} = {x: 5};//y等于5,默认值为3

如果变量名和属性相同,默认值可以简写为:var {x = 3} = {};

函数参数使用解构:

    function add([x, y]){
        return x + y;
    }
    add([1, 2]); //返回3

除参数可使用解构外还可以用于函数返回值,这样能实现便捷返回多个值

迭代遍历:

对于可迭代对象(实现Iterator 接口的对象),可使用for/of语句遍历:

    var map = new Map();
    map.set('first', 'hello');
    map.set('second', 'world');
    for (let [key, value] of map) {
        console.log(key + " is " + value);
    }

无论是数组还是对象,解构时都可以使用复杂的嵌套结构(左右对称,相当于模式匹配),当无值(值全等为undefined)时采用默认值,如果没有提供默认值则赋值为undefined,如果默认值是函数,则将在无值时才调用;右侧在适当时将进行类型转化,如右侧为原始类型时将转化为其包装对象

 

ES6代理对象:

ES6原生提供了Proxy对象,用于修改对象某些操作的默认行为,原生实现代理模式,示例如下:

    var obj = new Proxy({}, {
        get: function (target, key, receiver) {
            console.log(`getting ${key}!`);
            return target[key];
        },
        set: function (target, key, value, receiver) {
            console.log(`setting ${key}!`);
            return target[key] = value;
        }
    });
    obj.count = 1 //  setting count!
    ++obj.count//  getting count! setting count!

核心代码是:

var proxy = new Proxy(target, handler);

其中target参数表示要被代理的目标对象,handler参数也是一个对象,用来设定代理行为,handler里面可以指定13中代理操作,但这些操作发生时,是先调用handler参数里面的处理函数,如果没有处理函数将自己作用在被代理对象上,这13种操作如下:

get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']

set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。

has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。

deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。

getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。

preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。

isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。

setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)

construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

由于篇幅有限,不展开细讲,以下列出注意事项:

如被代理对象内部有this关键词,其指向代理对象,由于该原因有些对象不能被代理;代理对象可以作为原型对象,其上的设置可以被继承;注意和存取器属性的区别。

 

ES6反射Reflect

ES6提供了一个反射对象Reflect,将对象的一些明显属于语言内部的方法作为该对象的属性,属于语言的改进,让写法更优雅,Reflect对象一共有 13 个静态方法,与Proxy对象的13个操作是一一对应的。

 

异步编程:

在理解异步编程前,先看一下以下概念及其解释:

并发Concurrency同一时间段有多个程序都处于已启动到已结束之间,并且这几个程序都在同一个处理机上运行,单核处理器中多个进程被交替执行,表现为同时处理多任务。

并行Parallelism同一时刻(不是时段)有多个程序都处于已启动到已结束之间,和并发一样都表现为同时处理多任务;在多核处理器中,进程不但可以并发,还能并行;这里云客以推箱子举例,比如一个任务就是一个箱子,一个人轮流推着各箱子前行,总的看来这些箱子都在前行,但同一时刻这个人只在推一个箱子,这就叫并发,如果一个箱子分配一个人来推,也是一起前行,但这是真正的同时,这就叫并行,并发是在同一实体上交替,并行是多个实体上同时;也可以说并发是交替做不同事情的能力,而并行是同时做不同事情的能力;并发和并行都会出现同步和互斥两种关系。

互斥:进程之间访问临界资源时相互排斥的现象。

同步:进程之间存在依赖关系,一个进程结束时的输出作为另一个进程的输入。

多线程:多线程是进程中并发运行的一段代码,能够实现线程之间的切换执行。

异步:和同步相对,同步是按顺序执行,执行下一步前必须等待上一步完成,而异步是彼此独立,在等待某事件的过程中继续做自己的事,不必等待这一事件完成后再工作;线程是实现异步的一个方式,异步是让调用方法的主线程不需要同步等待另一个线程的完成,从而让主线程干其他事情。

异步和多线程:不是同等关系,异步是目的,多线程只是实现异步的一个手段,实现异步可以采用多线程技术或者调用其他进程来处理。

js程序采用单线程执行的,在任何时刻都只能做一件事,既不并发也不并行,这保证了编程的简单性,但会异步,事件、网络请求、定时执行都是异步执行,这让人感觉矛盾,实际上js本身并不具备异步能力,但js的宿主环境(比如浏览器或Node)是多线程的,宿主环境开辟新线程使得js具备了异步能力,开辟的新线程并不执行js代码,而是执行如文件下载、鼠标动作监听等,事件发生后通知js主线程去执行设置的回调函数,以浏览器说明,浏览器下的常驻线程有:

js线程

GUI渲染线程,(它与javaScript线程是互斥的)

事件线程(onclick,onchange,)

定时器线程(setTimeout, setInterval)

异步http线程(ajax)

在浏览器中js的执行分两个阶段:

同步文档加载阶段:

载入文档,浏览器分配一个主线程来执行js,之后无论在什么时刻都有且仅有这一个主线程在运行js程序,该阶段主线程按顺序同步执行js程序,载入文档过程中遇到document.write();输出的文档,视为载入的文档,并按顺序被一起渲染,渲染由另外的浏览器线程负责,到完成派发DOMContentLoaded事件后,意味着本同步文档加载阶段结束,该事件时图片文件可能还未被下载完成;这个阶段不会响应任何事件,往往会设置一些事件处理程序,又叫事件回调函数,在下一阶段时这些回调函数将按事件发生顺序被放入一个列队依次执行。

异步事件驱动阶段

这阶段文档已经完全载入并可以操作了,转由事件驱动执行,派发的第一个事件是load,浏览器开辟许多线程执行事件程序,比如下载文件、监听鼠标,注意这并不是执行js中的事件回调函数,一旦有事件发生,浏览器就将事件代码放入一个列队中,事件代码才是前文讲的事件回调函数,这阶段那个唯一的js主线程会不停读取这个列队,并依次执行,如列队中没有回调,js主线程将处于空闲状态,一旦有事件发生就放入回调,然后被读取执行,由此一直循环下去,形成了一个事件循环;js自始至终都是单线程的,仅那个主线程在执行,可以看出同一时刻只能响应一个事件,执行完一个事件代码后才能从列队中读取下一个事件的处理代码,在每个事件代码中都是同步的,如果被阻塞,则其他事件的响应需要等待。

看以下代码:

    setTimeout(function () {
        console.log("b");
    }, 500); //五百毫秒后执行
    var s = new Date();
    while (true) {
        let d = (new Date()) - s;
        if (d > 2000) {
            console.log("a");//阻塞,死循环两秒,
            break;
        }
    }

将等待两秒后先输出a,然后立即输出b,而并不是五百毫秒后先输出b,这是因为js是单线程执行,同步阶段被阻塞了两秒,虽然早在五百毫秒时就将setTimeout设置的回调放入了列队,但js线程没空(还在执行同步代码),所以一直等到同步阶段执行完成,进入异步阶段才从列队读取执行

 

事件循环:

其实事件循环并不那么简单,因为有优先级需要,所有任务(前文讲的事件回调函数)被分为两类:宏任务(Macrotask)和微任务(Microtask),鼠标事件、键盘事件、ajaxsetTimeout等属于宏任务,process.nextTickPromiseA.then()MutaionObserver属于微任务,实际上有两个列队来分别保存这两种任务,在同步文档加载阶段结束后首先执行微任务,依次执行完微任务列队后才读取第一个宏任务,执行完后继续执行微任务列队中的全部任务,然后再执行下一个宏任务,一个宏任务加一列队微任务就构成了一轮事件循环,见以下代码:


    Promise.resolve().then(() => {
        console.log('Promise1')
        setTimeout(() => {
            console.log('setTimeout2')
        }, 0)
    }).then(() => console.log('Promise2'));

    setTimeout(() => {
        console.log('setTimeout1')
        Promise.resolve().then(() => {
            console.log('Promise3')
        })
    }, 0)

将依次输出(Promise对象见下一节):

Promise1 
Promise2 
setTimeout1 
Promise3 
setTimeout2

换一个角度,如果将同步文档加载阶段的所有js视为一个宏任务的话,js主线程就是先执行宏任务然后是一列队微任务,完成一轮事件循环,如果不这样看,那么首先执行的就是一列队微任务然后是一个宏任务,完成一轮事件循环;不管是宏任务还是微任务,在任务内都是同步执行,且任务间都按在列队中的顺序执行,浏览器在每个宏任务开始之前渲染页面

后文会多次提到“异步执行”,也就是立即将回调放入事件列队中等待执行

注意:在异步事件驱动阶段如果使用document.write();则各浏览器行为有差异,火狐相当于载入新文档,所有js失效,chrome表现的像重置了DOM,但js继续有效,建议不要在异步阶段用document.write();

 

Promise对象:

ES6为异步编程提供了Promise对象,这是一个绑定了回调的状态对象,用于状态改变时执行其上绑定的相应回调,有三种状态:等待中pending、成功fulfilled、失败rejected,状态只可能是从等待中变为成功或失败,且一旦发生改变即不会再变,示例如下:

    var promise = new Promise(function (resolve, reject) {
        let f = function () {
            let num = Math.floor(Math.random() * 10);
            if (num % 2 == 0) {
                resolve('偶数');//触发状态改变为成功,并传出参数,参数是任意的,比如对象
            } else {
                reject('奇数'); //触发状态改变为失败,并传出参数,参数是任意的,比如对象
            }
        }
        setTimeout(f, 3000);//设置一个时间事件
    });
    promise.then(function (arg) {
        console.log(arg)
    }, function (arg) {
        console.log(arg)
    });

该例在一定时间后promise状态发生改变,并在改变后立即执行then绑定的动作,解释如下:

Promise对象由构造函数Promise实例化,实例化时构造函数接收一个叫做执行器executor的函数并立即执行,将忽略其返回值,执行器接收两个参数,参数名随意,这里使用了resolvereject,这两形参会接收js引擎提供的两个触发函数,这两触发函数分别用于在异步事件发生后触发promise对象的状态转变为成功和失败(第一个转为成功,第二个转为失败),并传递事件结果或失败原因,传递的值会一直保存在promise对象上,称为状态值。

promise对象有两个关键内容是:状态以及状态改变后保存的状态值;状态决定调用成功还是失败回调,状态值作为回调的参数传递,一旦状态转变,将异步执行绑定的成功或失败回调,回调接收状态值做参数;在状态改变后新绑定的回调也会异步执行,同样接收状态值做参数。

promise对象的then方法用于绑定状态发生改变后执行的异步回调,接收两个回调做参数:

第一个处理成功,如果传递的参数不是函数,则以“function(x){return x;}”代替

第二个处理失败,是可选的,如果省略,则效果相当于“function(x){return Promise.reject(x);}

绑定的回调是微任务,在下一个宏任务前执行,回调都以promise对象的状态值做参数,then方法将返回一个新的Promise对象,其状态和状态值仅和回调有关,和调用她的Promise对象无关,规则如下:

    如果回调有返回值,那么是成功状态,并且将返回值作为状态值。

    如果回调没有返回值,任然是成功状态,状态值为 undefined

    如果回调抛出错误,那么是失败状态,状态值为抛出的错误。

    如果回调返回一个已是成功状态的Promise,那么是成功状态,状态值为返回Promise对象的状态值。

    如果回调返回一个已是失败状态的Promise,那么是失败状态,状态值为返回Promise对象的状态值。

    如果回调返回一个未定状态的Promise,那么也是未定的,且终态相同,终态状态值也相同。

以上规则中所指回调既可以是成功回调也可以是失败回调,由于then方法返回新Promise对象,因此可以采用链式调用法,链中后面的回调接收前一个then方法返回的Promise对象的状态值做参数,而不能简单的说后面的回调接收前面回调的返回值,因为如果前面回调返回的是Promise对象时,后面回调接收的并不是对象本身,而是其上的状态值。then方法为连续执行多个异步操作提供了解决办法,避免了js中经典的回调地狱,其名字即为“接下来做”的意思;如果不采用链式写法,而是在原Promise对象上多次调用then方法,这意味着绑定的回调间不存在先后依赖关系,会按绑定顺序依次执行

then方法是定义在Promise构造函数的原型属性Promise.prototype上的,原型上还有两个方法:

catch方法:

用于失败时执行回调,是then(undefined, onRejected)的简写,通常用于链式调用中集中处理失败,示例:

    promise.then(function (arg) {
        console.log('ok'+arg);
        return arg;
    }).then(function (arg) {
        console.log('1ok' + arg);
        return 'arg';
    }).catch(function (arg) {
        console.log('err:' + arg);
        return arg;
    });

如果一个Promise 对象没有任何处理失败的回调,那么在失败时将向上抛出异常

 

finally方法

Promise 对象状态改变后,无论成功还是失败,在执行then()catch()后,都会执行finally指定的回调函数,该方法返回一个新Promise对象,与then(onFinally, onFinally) 类似,但回调不接受参数,且返回的Promise对象和回调无关,而是和调用她的Promise对象相同,但如果在finally回调中抛出错误或返回失败状态的promise对象,则finally方法返回的promise对象将是失败状态,状态值是抛出的错误或返回的那个失败promise对象的状态值,示例如下:

    Promise.resolve(2).finally(
        function () {
            //正常情况finally方法返回调用自己的promise
            //return Promise.reject(3); //此时finally返回的promise为失败,且状态值为3
            //throw 4;//finally返回的promise为失败,且状态值为4
        }
    ).then(function (arg) {
        console.log('ok'+arg);
        return arg;
    },function (arg) {
        console.log('no' + arg);
        return arg;
    })

 

构造函数Promise返回的Promise对象的状态和状态值:

这并不能简单的看执行器调用的是resolve还是reject触发器,还和触发器传递的值有关,如果传递的是简单值,那么只需要看调用的是哪种触发器即可,但如果传递的是另外一个Promise对象,那么就变的不一样了,见如下示例:

      let p0 = new Promise(function (resolve, reject) {
          setTimeout(() => {console.log('p0');resolve('ok')}, 3000);
      });
      let p1 = new Promise(function (resolve, reject) {
          setTimeout(() => {console.log('p1');reject(p0)}, 2000);
      });
      let p2 = new Promise(function (resolve, reject) {
          setTimeout(() => {console.log('p2');resolve(p1);}, 1000);
      });
      p2.then(function (result) {
          console.log(result);console.log('result')
      }, function (error) {
          console.log(error);console.log('error')
      });

通常实践中不推荐使用以上代码,而应该使用then链代替。

在该例中从p2p0形成了一个链条,字面看实例化p2时,执行器调用的是resolve触发器,表面看p2状态应该是成功,状态值应该是p1,但实际上并非如此,p2的状态和状态值由该链条决定,会从p2p0方向判断执行,如果链中都为成功时,结果为成功,状态值为最后一个Promise对象的状态值(该例p0即为最后一个);链中只要有一个是失败的,结果状态就是失败的,状态值为第一个失败的Promise对象的状态值(上例中p2的状态值即为p0);在链条判断过程中,在没有出现失败前会依次等待链中的状态改变,一旦有失败出现,则不会再等待,将立即决定最终的状态,只有确定了状态才会执行回调函数。

有个简单的办法来判断构造函数Promise返回的Promise对象的状态和状态值:

将执行器里面的resolvereject分别替换为下文的Promise.resolve(value)Promise.reject(reason),她们的返回值即为构造函数返回的Promise对象

如果在执行器函数中抛出一个错误,那么返回的promise对象状态为失败rejected,但执行器中异步函数里抛出的错误不会:

var promise = new Promise(function (resolve, reject) {
        let f = function () {
            //throw "异常"; //在此处抛出异常将导致无捕获
        }
        setTimeout(f, 1000);//设置一个时间事件
        //throw "异常"; //导致返回的promise为失败
    });
    promise.then(function (arg) {
        console.log('ok'+arg);
        return arg;
    }).catch(function (arg) {
        console.log('err:' + arg);
        return arg;
    });

 

构造函数Promise上有一些有用的方法:

Promise.all(iterable)

    用于处理多个promise对象的状态集合,返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。

Promise.race(iterable)

    iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。

Promise.reject(reason)

    返回一个状态为失败的Promise对象,并将给定的失败信息作为状态值

Promise.resolve(value)

返回一个状态由value决定的Promise对象。如果valuethenable(即带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则 (value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将value做状态值。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该valuePromise对象形式使用。

 

异步函数async

异步函数是对Promise对象的更高一级包装,是ES2017引入的,先看示例代码:

    function resolveAfter2Seconds(x) {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(x);
            }, 2000);
        });
    };
    async function add(x) {
        var a = await resolveAfter2Seconds(20);
        var b = await resolveAfter2Seconds(30);
        return x + a + b;
    }
    add(10).then(v => {
        console.log(v);  // 4 秒后打印 60
});

异步函数使用“async function”关键词声明,内部的“await”仅能用于异步函数,其后应是一个Promise对象,意为暂停并等待Promise对象产生状态值后再继续执行当前异步函数,“await”表达式的返回值是成功的Promise对象的状态值,如果Promise对象失败将抛出异常(该异常不属于异步函数抛出的异常,但在异步函数内可以捕获),如果“await”后面不是Promise对象,那么在内部会将其传递给Promise.resolve(value)方法转化为Promise对象,整个异步函数调用最终的返回值会隐式转换为Promise对象后再返回,转换规则和Promise对象的then方法返回Promise对象的规则一样,换句话说,返回值(无返回值时为undefined递给Promise.resolve(value)方法转化为Promise对象后再返回,如果异步函数内抛出未捕获的异常,那么返回失败的Promise对象,其状态值为抛出的异常。

异步函数也可以写成表达式方式:

var add = async function(x) {}

同样可对异步函数立即执行:
 

(async function (x) {})();

 

js代码放置位置:

外部独立文件方式:

<script src="/js/example_externaljs.js">
</script>

由于js是默认脚本语言,因此可以省略“type="text/javascript"”,外部文件中不包含 <script> 标签,是纯js代码,实际上外部文件可以不是一个真正的文件,可以是php动态输出的(需要输出正确的MIME),因此可以用url传递参数;外联脚本的执行默认是同步和阻塞的,换句话说就是会阻塞DOM构建和文档UI渲染,但标签里面可以放置deferasync属性,她们都表示该外联js不要阻塞DOM构建和文档UI渲染,都只适用于外联脚本,区别如下:

defer:表示延迟执行,在文档载入和解析完成后,DOMContentLoadedload事件之前执行,如果有多个defer外联js,将按位置顺序执行

async:表示异步执行,一旦被载入,就立即执行,执行过程中文档将暂停解析,直到脚本执行完毕,如果有多个外联js文件,将按载入时间执行,因此顺序是不可预测的

如果这两个属性都出现,很多浏览器以async优先,使用这两属性的js不应该使用document.write();语句。

内部直接放置代码:

<script type="text/javascript">
alert("Hello World!")
</script>

可以放于head之间或body之间,当页面载入时会按放置顺序执行JavaScript。最好将函数放于head 部分,确保调用时已经被加载,现代前端强调js程序和文档分离,尽量不要用该方式,首选外部代码方式。

事件属性方式:

如:

<button onclick=" f1(),f2()">点击</button>

可包含任意条语句,用逗号分隔,不推荐,原因同上

url方式:

如:<a href="javascript:void 0;">点击</a>,以协议“javascript:”开始,可包含任意条语句,用分号分隔,返回最后一条语句的值,不同浏览器对待返回值的方式不同,该列中火狐点击将显示返回值,chrome则忽略,通常如没有返回值将表现的点击无反应,但语句还是会被执行。不推荐,原因同上

 

模块:

模块是ES6引入的,在理解她前先考虑一下这些问题:现在很多网站项目都很大,前端的js会很多,比如著名的Drupal系统默认首页大约有个80多个js文件,这些js不可能是一个人编写的,那么就极可能会出现命名冲突,虽然有一些办法可以避免但都比较拙劣,再者js的依赖关系全靠script标签的位置,而排序标签的人可能并非js作者,要搞懂这些依赖关系是比较困难的,依赖应该由js作者负责,因此需要模块化机制,在服务器端更加需要,实际上模块化思想就是从后端发源的。于是ES6在语言层面实现了模块功能,加载一个模块文件:

<script type="module" src="./foo.js"></script>

默认是defer方式加载的,如果声明了async属性,则以async为准,模块也可以用内联写法:

<script type="module">
    console.log('云客');
</script>

type="module"来表明是一个ES6模块,加载后会默认执行,一个模块文件就是一个普通的js文件,但有以下注意点:

1、代码处于模块作用域之中,而非全局作用域,即便模块内部的顶层变量,外部也不可见,全局变量在模块中有效。

2、模块脚本自动采用严格模式,不管有没有声明use strict

3、模块内顶层的this关键字返回undefined,而不是指向window

4、同一个模块如果加载多次,将只执行一次。

 

模块内可使用import命令导入其他模块提供的内容(也叫提供的接口),也可使用export命令对外导出内容,exportimport只能用于模块中,且必须在顶层,因为底层是在预编译阶段执行,所以不可用在条件语句等运行时逻辑中

导出export

export 语句用于从模块中导出函数、对象、类或原始值等,以便其他程序可以通过 import 语句使用它们,导出的内容也处于严格模式,使用如下:


export { name1, name2, …, nameN };//按原来名字导出
export { variable1 as name1, variable2 as name2, …, nameN };//取别名后导出
export let name1, name2, …, nameN; // 声明的同时导出,也适用于var和const
export let name1 = …, name2 = …, …, nameN; // 声明赋值的同时导出,也适用于var和const
export function FunctionName(){...} //导出函数
export class ClassName {...} //导出类
//以下为导出默认内容,一个模块仅可导出一个默认内容,而不管导出的是什么
export default expression; // 导出表达式返回的内容
export default function (…) { … } // 也适用于 class、function*
export default function name1(…) { … } //也适用于 class, function*
export { name1 as default, … };//将某变量当做默认内容导出
//以下是重定向导出,也就是从其他模块取内容再导出,取的内容在本模块中无效
export * from …;//导出的是取到的所有内容
export { name1, name2, …, nameN } from …;//导出的是部分内容
export { import1 as name1, import2 as name2, …, nameN } from …;//别名
export { default } from …; //从其他模块取默认内容,并作为默认内容导出

可多次使用export导出,其中nameN是要导出的标识符,以便被其他脚本通过 import 导入,不可以导出原始值,

导出的不是值拷贝,而是引用,换句话说模块内变化后,外部也会实时变化,导出的内容在外部是只读的,不可修改,但如果导出的是对象则外部可以为其添加或修改属性,且修改会反应到模块内,这表现的像常量。

 

导入import

import语句用于导入其他模块通过export导出的内容,用法如下:

import defaultExport from "module-name"; //导入模块的默认导出
import * as name from "module-name"; //导入所有导出的内容,并构建成一个对象,导出内容是name的属性
import { export } from "module-name"; //导入某一个导出内容,名字和导出时相同
import { export as alias } from "module-name"; //同时,但取别名
import { export1 , export2 } from "module-name";//导入多个导出内容
import { export1 , export2 as alias2 , [...] } from "module-name"; //可部分取别名
import defaultExport, { export [ , [...] ] } from "module-name"; //导入默认导出和命名导入混用
import defaultExport, * as name from "module-name"; //导入默认导出和命名导入混用
import "module-name";//不导入任何内容,仅仅起到加载并运行模块的作用

只有导出的内容才能被其他模块导入,其中模块名通常是一个绝对或相对 URL(不能省略.js后缀),相对地址是相对于本模块地址的,而不是当前文档地址;在模块中已通过import导入了某模块,那么被导入的模块不需要再在网页中通过script标签加载,这就实现了完全由模块开发者控制依赖关系。

 

动态导入:

import命名仅在编译时执行,运行时可以使用函数import('module-name')导入,她返回一个promise对象:

import('./myModule1.js')
    .then((myModule) => {
        myModule.yunke();
    }).catch(err => {
    console.log(err.message);
    });

其中myModule是一个代表模块的对象,导出内容是其属性,但动态导入目前还处于提案阶段,经测chrome有实现,火狐需要先开启,Edge不支持

 

浏览器对象模型BOM

针对html文档有DOM(文档对象模型),DOM可以让js操控html文档,针对浏览器也有个BOM,它可以让js操控浏览器,称为浏览器对象模型,BOM提供了一些属性和方法:

Window对象:

js中所有全局变量、全局函数都是该对象的属性或方法,引用该对象的属性或方法往往可以省略window.变量名,它提供了一些方法(有些浏览器出于安全、隐私或骚扰原因会禁用一些方法):

    window.open() - 打开新窗口
    window.close() - 关闭当前窗口
    window.moveTo() - 移动当前窗口
    window.resizeTo() - 调整当前窗口的尺寸

 

location对象

是窗口对象的属性:window.location,可简写为location,和document.location全等,提供和当前url相关的属性和方法,比如:

    location.hostname 返回 web 主机的域名
    location.pathname 返回当前页面的路径和文件名
    location.port 返回 web 主机的端口 (80 或 443)
    location.protocol 返回所使用的 web 协议(http:// 或 https://)
    location.reload();重新加载文档

可以直接赋值location中上述的这些属性以跳转页面,如果直接赋值location通常应当使用完整url以供内部解析分别赋值

 

document.cookie对象:

是窗口对象的属性:window. document.cookie,可简写为document.cookie,可以设置和取回cookie,如果cookie被设置了仅http可访问,则无法取回

 

计时:

定时执行代码:

var t=setTimeout("javascript语句",毫秒)

该函数在设定的毫秒时间后开始执行代码,返回一个递增的整数做任务标识,如果需要每隔一段时间就执行代码,请用以下函数:

var t= setInterval("javascript语句",毫秒)

如果需要取消任务,则将任务标识传递给以下函数:

clearTimeout(t)

 

警告框、确认框、提示框:

警告:alert("文本");,点击确定后才继续执行js

确认:confirm("文本");,从用户处获取是或否,返回布尔

输入提示:var in=prompt("请输入值","默认值");,从用户处获取输入值

 

navigator对象:

是窗口对象的属性:window.navigator,可简写为navigator,包含有关访问者浏览器的信息,比如版本、语言、操作系统等,因为用户可以设置浏览器,因此有时该对象的信息并不可靠。

 

history对象:

是窗口对象的属性:window.history,可简写为history,由于安全原因该对象被限制访问,可使用这些方法:

history.back() :与在浏览器点击后退按钮相同
history.forward() :与在浏览器中点击向前按钮相同

 

screen对象:

是窗口对象的属性:window.screen,可简写为screen,有许多和屏幕相关的属性,比如:

    screen.availWidth - 可用的屏幕宽度
    screen.availHeight - 可用的屏幕高度

以像素计,减去界面特性,比如窗口任务栏。

 

常用方法:

打印变量

console.log(a,b);,向浏览器控制台输出一个或多个变量信息,这类似phpprint_r($a);

查找元素:

通过id查找:

var e=document.getElementById("yunkeID");

通过标签名查找:

var e=document.getElementsByTagName("p");

通过类名查找:

var e=document.getElementsByClassName("yunkeClass");

修改元素:

改变元素内容:

document.getElementById("p1").innerHTML="New text!";

改变属性:

document.getElementById(id).attribute=new value
如:document.getElementById("image").src="landscape.jpg";

改变样式:

document.getElementById(id).style.property=new style
如:document.getElementById("p2").style.color="blue";

创建元素:

var para=document.createElement("p");
var node=document.createTextNode("这是新段落。");
para.appendChild(node);
var element=document.getElementById("div1");
element.appendChild(para);

删除元素:

var child=document.getElementById("p1");
child.parentNode.removeChild(child);

 

事件:

响应js事件的通用事件属性(绑定事件和js代码用“元素对象.事件属性名=JavaScript代码”):

onabort 图像加载被中断

onblur 元素失去焦点

onchange 用户改变域的内容

onclick 鼠标点击某个对象

ondblclick 鼠标双击某个对象

onerror 当加载文档或图像时发生某个错误

onfocus 元素获得焦点

onkeydown 某个键盘的键被按下

onkeypress 某个键盘的键被按下或按住

onkeyup 某个键盘的键被松开

onload 某个页面或图像被完成加载

onmousedown 某个鼠标按键被按下

onmousemove 鼠标被移动

onmouseout 鼠标从某元素移开

onmouseover 鼠标被移到某元素之上

onmouseup 某个鼠标按键被松开

onreset 重置按钮被点击

onresize 窗口或框架被调整尺寸

onselect 文本被选定

onsubmit 提交按钮被点击

onunload 用户退出页面

这些事件属性只能绑定一个处理程序,如果需要绑定多个,可以使用addEventListener()方法

 

AJAX

关键在于创建 XMLHttpRequest 对象:

<script type="text/javascript">
    function loadAJAX (url, id) {
        var xmlhttp;
        if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
            xmlhttp = new XMLHttpRequest();
        }
        else {// code for IE6, IE5
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        xmlhttp.onreadystatechange = function () {
            if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
                document.getElementById(id).innerHTML = xmlhttp.responseText;
            }
        }
        xmlhttp.open("GET", url, true);
        xmlhttp.send();
}
</script>

js函数用于AJAX无刷新取数据(不能跨源访问),url参数为向服务器获取数据的地址,ID为取回数据后显示在为该ID值的元素内,将上面代码放入<head></head>内,在需要点击的元素里加入:

onclick="loadAJAX(“服务器地址”, “本页元素的ID”)" 括号内加入参数即可

 

同源安全策略:

同源策略限制js能操作哪些web内容,为了安全起见,浏览器会阻止一个源中的js去操作不同源的文档及不同源js交互,相同源是指主机名、端口、协议均相同,这三者任意一个不同即属于不同源。对于js代码来说,它的源和脚本代码本身的来源没有关系(可以来自任何地方),而是和嵌入脚本的文档的来源有关,换句话说文档嵌入了来自其他域名的js,则被嵌入的js和文档是属于同源的,可以操作文档,这意味着嵌入第三方js将授予其完全控制页面的权力。

同源策略几乎运用于不同源的所有对象的所有属性,但实际上有极少属性是不适用的,比如js打开一个不同源的新窗口后,也可以关闭它,但这些另外都是以安全为前提,即便js能打开不同源窗口,也不能查看其内部信息,同源策略有伸缩性,比如同一个顶级域名下的各子域名对应的文档,只需要把属性“document.domain”设置为相同即可避开同源策略的限制(但该方法只限有相同顶级域名的情况)。

 

文档间作用域:

页面可以通过iframe标签包含多个文档(H5已弃用<frameset> <frame>),每个文档有独立的window对象,由于js中所有全局对象和函数都是window对象的属性,因此每个文档里面的js在各自的作用域下工作,互不影响,如果需要跨文档访问则需要看是否同源而定:

如果不同源则互相之间不可访问,此时可以使用跨文档消息传递机制,如postMessage()

如果同源则可以通过引用彼此的window对象来相互操作,比如:

<iframe src="i.html" width="500px" height="400px" id="yunke"></iframe>

在父窗口中引用子窗口的window对象,有几种写法:

var childWindow=document.getElementById('yunke').contentWindow;
var childWindow=window.frames.yunke.contentWindow;
var childWindow=window.frames['yunke'].contentWindow;
var childWindow=window.frames[0];//注意数字索引直接保存window对象

在子窗口中引用父窗口对象用全局变量:parent,引用层级中的顶级窗口用:top

引用到窗口的window对象后,就可以通过其属性访问该窗口的全局变量,通过其提供的API操作DOM了,如:

        var childInput=childWindow.document.getElementById('childInput');
        childInput.value="设置子窗口表单值成功";

示例代码如下:

父窗口:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>窗口间交互</title>
</head>
<body>
<button onclick="p1()">点击设置子窗口表单</button>
父窗口表单:<input type="text" id="parentInput"><br>
<iframe src="i.html" width="500px" height="400px" id="yunke"></iframe>
<script type="text/javascript">
    var childWindow=window.frames.yunke.contentWindow;
    var myStr="父窗口设置成功";
    function p1() {
        var childInput=childWindow.document.getElementById('childInput');
        childInput.value="设置子窗口表单值成功";
    }
    function p2() {
        console.log('父窗口函数p2');
    }
</script>
</body>
</html>

子窗口:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>窗口间交互子窗口</title>
</head>
<body>
<button onclick="c1()">设置父窗口表单值</button>
<button onclick="c2()">调用父窗口方法</button><br>
子窗口表单:<input type="text" id="childInput"><br>
<script type="text/javascript">
function c1() {
    var parentInput=parent.document.getElementById('parentInput');
    parentInput.value=parent.myStr;
}
function c2() {
    parent.p2();
}
</script>
</body>
</html>

 

常见js框架:

js框架主要用于解决浏览器兼容问题、提供界面ui、动画、操作DOMajax等等,常见的有:

jQueryPrototypeMooToolsYUIExt JS Dojo script.aculo.us UIZE

 

 

补充:

1、参考资料:

标准制定官网:http://www.ecma-international.org

官方标准文档:http://www.ecma-international.org/ecma-262/6.0/

火狐社区资料:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript

微软参考资料:https://docs.microsoft.com/zh-cn/microsoft-edge/dev-guide/whats-new/jav…

国内参考资料:http://es6.ruanyifeng.com/#docs/intro

Node.js官方中文文档:https://nodejs.org/zh-cn/

Node.js中文非官方文档http://nodejs.cn/api/

 

 

后记:

在写完本篇时,云客作为一个phperjs有两个深深的感受,既觉可爱又为其感到无奈,可爱不用多说,js大大提高了项目在用户端的控制能力,将web变成了应用;一路发展到今天非常不易,从浏览器厂家的纷争到如今的标准统一,每一次改进都要面对历史的兼容,不可能抛开存量web资源而跨越式发展,每添加一个功能都小心翼翼,这些都让人感觉不易,倍感珍惜。

无奈之处是因为js的野心,目前js在扩张,不但可以在浏览器、服务器中执行,还可以用来开发桌面程序,这看似强大,但忽略了一些深层次因素,在各种环境下开发最重要的区别并不是语言,而是这个环境的特性,不同特性需要有与之精确匹配的开发语言和开发思维,专用的通常好于通用的,js在诞生之初就用于浏览器,伴随浏览器发展演进,很多特性是为浏览器准备的,并没有考虑其他环境,甚至会阻碍其他环境;比如桌面环境很多是需要多线程的,服务器环境很多功能是交给类似apache这样的服务器软件完成的;ES6为了让js能进行大型项目的开发改进了不少,但这种改进也让人感受到深深的无奈,首先大型项目最重要的就是按接口开发,但js没有interface功能,缺乏接口约束,为了不得不做的兼容又不能跨越式改进,这让人感觉两难,如果真大幅改进那还不如发明一种新语言。

联想到一个例子,可能更加形象:人类在认识大自然的过程中,面对原子分子这些微观事物时,发展出了一整套方法,因此建立了化学学科,当面对的是大分子团时,粒子有基本的组成单元,看到的是各种有共性的单元而非基本粒子,需要改变下研究的方法,因此发展出了有机化学,进一步在研究动植物等生命体时,发现很多更高一级的基本构成是相同的,尺度更加宏观,因此建立了生物学,此时生物学的方法和化学已经差异很大了,这就像吃饭,用牙签可以吃的很仔细(强大灵活),但我们会用筷子(简单高效),js的无奈便是生于化学,强大灵活,却梦想这兼备生物学的简单高效,想要拥抱真正的接口、类却又带着原型链、函数式,何去何从呢。

 

本文作者:云客【云游天下,做客四方】微信号:php-world,欢迎转载,但须注明出处,交流请加qq203286137

作者博客:https://blog.csdn.net/u011474028

 

本书共126小节:

评论 (写第一个评论)