0、webpack配置、安装
<Option key={'全部'} value= {''} >全部</Option> es6: 块级作用域:{}, let 常量:const ,分配后的值的过程不能改变 解构数组:let [v1, v2, v3] = function(){ return [1, 2, 3]; }(); console.log(v1, v2, v3); 解构对象:let {v1:v1, v2:v2, v3:v3} = function(){ return { v1:2, v2:4, v3:6 } }(); console.log(v1, v2, v3); 模版字符串:`dddd,${变量名}` 反单引号之间可以任意换行 带标签模版字符串: let dessert = 'xx', drink = 'ddd'; let bf = kitchen `今天早餐是\n ${dessert} 与 ${drink}!`; function kitchen(string, ...values) { let result = ''; for(var i = 0; i < values.length; i++) { result += strings[i]; result += values[i]; } result += strings[strings.length - 1]; return result; } console.log(bf); startsWith|endsWith|includes字符串是否开头|结束|包含 函数参数的默认值:function aa(d = 'd', w = 'w'){}; ...:展开数组 传递多个参数形式:function cc(arg1,arg2,...args) 解构对象参数 函数名字属性[name]:保存的是函数的名字 箭头函数: let 函数名 = (参数列表) => {语句块} 对象表达式:{ dessert, drink, show(){} } 两个值是否相等:Object.is(); 复制对象值到另一个对象:Object.assign(om1,oy2); 设置|获取对象的prototype: Object.set|getPrototypeOf(); 创建一个对象:Object.create(o); es6的__proto__可以修改设置 <=> prototype 继承了指定对象(属性、方法); super:拿到父类的对象; Iterators:迭代器 => {value:x, done:true|false}; next(value, done); Generators迭代生成器 手动创建迭代器: function iterators(arr) { let i = 0; return { next() { let done = (i >= foods.length); let value = !done ? foods[i++] : undefined; return { value:value, done:done } } } } function* iitt(arr) { for(var i=0; i<arr.length; i++) { yield arr[i]; } } var iterator = iitt([1,3,5]); 类:class Chef{ constructor(food) { this.food = food; //类属性 } cook() { //类方法 console.log(111); } get 方法() { return 属性之类的 } set 方法() { 设置操作 } } console.log(对象.方法 = 值) //通过set方法,设置值 console.log(对象.方法) //通过get方法,获取值 类中的静态方法:在方法名加个关键字static。既可, 静态方法不能访问对象方法 继承:extends class Person { constructor(name, birthday) { this.name = name; this.birthday = birthday; } intro() { return `${this.name},${this.birtyday}`; } } class Chef extends Per constructor(name, birthday) { super(name, birthday); } } Set:内对容不能重复 add|size|has|delete|clear|forEach(val) Map: key-value形式 set|size|has|delete|clear|forEach Module:模块 通过export导出变量、函数等模块:export {属性、方法名} 模块默认导出:在函数名称前加入export default; export {default as 方法|属性} import [模块[as 模块别名]...]| * as 名称 from 模块路径 导出具体模块|所有模块 导入、导出都可以取别名; webpack配置: npm init 生存json文件 npm install -g 包名| 包名 --save-dev npm update 包名 npm uninstall 包名 安装webpack : npm install -g webpack 模块加载器兼打包工具,支持AMD、CMD语法 打包命令:webpack 源码js文件地址 输出js文件地址 模块加载器:(各种不同文件类型的资源,webpack都有对应的模块加载器[loader]). 安装loader: npm install xxx-loader[] ... ]--save-dev require('style-loader!css-loader!../css/style.css'); webpack配置文件说明: module.exports = { entry: 打包入口文件路径([]), output: { //配置打包结果 path: __dirname + //定义输出文件路径, filename: //指定打包文件名称, }, module: { //定义对模块的处理逻辑 loaders: [{ //定义一系列加载器 test: //正则, loader: string|array,处理匹配到的文件, include: string|array 包含的文件夹, exclude: string|arge|array 排除的文件夹, } }, resolve: { extensions: ['', '.js', '.css', '.jsx'] //自动补全识别后缀 } };
1、模块化—seaJS手
一:Bower获取 要安装bower Npm install -g bower Bower install seajs 二:Use方法是整个项目的入口方法,通常一个项目中只调用一次即可 方法接受两个参数 第一个参数表示引入模块的路径 可以是一个字符串,此时引入一个文件 也可以是一个数组,每个成员表示一个文件地址 第二个参数是一个回调函数 作用是全局作用域 回调中的参数个数与前面加载的模块一一对应 三:Seajs中根目录就是seajs所在的目录; 在使用seajs时候,要将seajs放在最外面,这样方便我们加载模块。 Seajs的use方法返回值就是seajs对象,因此可以链式调用。 四:创建一个文件就是定义了一个模块,文件中我们必须通过define方法定义模块,如果写在define方法外面就不是模块的内容;因此属性,方法,变量等等要写在define里面,这样就是一个模块文件。 Define使用很灵活,有多种使用方法。 传递一个参数 是一个值类型的数据,这个数据就作为接口暴漏出来,如;字符串,数字,布尔值,null; 是一个对象,此时这个对象就是暴漏的接口,属性就是暴漏的属性,方法就是暴漏的方法,如({}); 是一个函数(很重要,90%以上都是这种方式),这种方式跟符合commonjs规范,此时有是三个参数,require[加载外部模块],exports[暴露接口],module[返回模块信息]。 作用域是全局作用域,因此不要通过this(它指向window)定义变量,会污染作用域 传递两个参数 第一个参数 是一个字符串,表示模块的名称id;或是一个数组,表示模块依赖的集合。 第二个参数是一个回调函数,跟上面的函数是一致的。 传递三个参数 第一个参数是个字符串,表示模块的id;[下次use的时候,就可以直接使用这个id,引入该模块]。 第二个参数是个数组,表示模块依赖的集合。 第三个参数是一个回调函数,跟上面的函数式一致的。 写在依赖集合中的依赖模块,无论你有没有使用它,它都会被加载。 五:require注意点: 第一点 定义模块的模块函数中参数require不能简写; 第二点 require不能修改[不能赋值给其他的变量、require不能赋值、require不能作为内部函数的参数、在子函数的内部,不能再修改require]; 第三点 require的参数不能是表达式; 在一个文件中如果define定义了多个模块时候,若没指定id,此时后面的模块会覆盖前面的;若指定了id,这些模块是兼容的,此时想使用哪个模块,require这个id即可。 Require是同步加载模块,加载就可以使用[均会加载];Require.async是异步的加载模块,只能在加载完成的回调函数中使用[条件成立才会加载] 六:exports定义接口的6种方式: 1.exports.接口 2.module.exports.接口 第一种方式和第二种方式是可以继续添加接口的 3.module.exports = {} 定义了所有的接口,不能在使用其他方式定义接口 4.module.exports = function(){} 定义的接口是函数接口,这个函数可以直接执行 定义的接口只一个就是函数,不能在添加其他接口属性了(不能在使用前面两种方式了) 5.return {} 这个对象就是暴漏的接口 定义的对象表示全部的接口,不能再用其他方式定义接口了,并且它的优先级大于前面四种方式 6.return function(){} 将方法作为一个接口 ||----------------------------------------------------------------------------------------|| 7.return 值类型 这个中方式跟module.exports = 值类型是一样的, 跟define传递值类型数据是一致的 8.module.exports = 值类型数据 9.define 一个参数时候,这个参数如果是值类型数据可以作为接口 10.define 如果是一个对象,这个对象可以作为一个暴漏的接口 ||----------------------------------------------------------------------------------------|| 定义接口的优先级 return > module.exports={} > module.exports.或者exports。 注意: 第三种和第四种方式使用时一定要注意前面是否使用了前两种定义接口的方式; 所以使用return要注意前面是否使用了其他方式,否则会覆盖掉; 想逐一定义接口,只能使用前两种方式。 七:module: 此对象里包含模块所有的属性信息,我们在模块内部可以通过这个模块来查看它的相关属性信息。 对象的一些属性 Id 表示模块的真实id,如果没有设置,他的值就是uri Uri 表示模块所在文件的地址。 若模块没有定义id,那么这个uri就是真实的地址; 若模块定义了id,那么uri的文件名就是id的名称。 Dependencies 依赖的模块,是一个数据,根据依赖集合构建的 Deps 依赖模块对象,每一个属性对应一个模块的id,属性值模块的对象 Exports表示模块暴漏的接口 Status 表示模块的状态 八:对于非seajs模块化的类库,我们需要手动定义它,通过define的CMD规范定义就可以正常使用了。 九:配置config seajs.config({ alias: { 'jquery': 'lib/jquery-1.12.2.js' }, // 简化路径书写的 paths: { lib: 'lib' }, // 如果我们压缩文件,如果我们将文件名称加上.min后缀,此时如果去模块内部修改写着模块成本很高,此时我们通过map来配置 map :[ ['main.js', 'main.min.js'] // ['.js', '.min.js'] ], // 定义路径模板变量 vars: { d: 'dom' }, base: 'js', debug: true, charset: 'gbk' });
2、js经验手记
1、我们习惯使用 setTimeout(function(){ alert(1111111) },1000); 这样使用也可以,后面是是回调参数 setTimeout(function(num){ alert(num); },1000,111111); 2、拼接字符串 window.onload = function(){ document.body.innerHTML = "<div>div</div><span>span</span><p>p</p>"; }; window.onload = function(){ document.body.innerHTML = "<div>div</div>"+ "<span>span</span>"+ "<p>p</p>"+ "55555555555"; }; window.onload = function(){ document.body.innerHTML = "<div>div</div>\ <span>span</span>\ <p>p</p>\ 55555555555"; }; 3、typeof使用 var arr = []; console.log( typeof arr); arr.num = 10;【可以这样加任何行为或是属性,这样length还是0】 console.log( typeof arr); console.log( typeof(arr));【1-3:object】 console.log(arr instanceof Object); console.log(arr instanceof(Object)); console.log( 'num' in arr); console.log( 'num' in(arr)); 【4-7:true】 4、停止嵌套循环 for(var i=0;i<5;i++){ for(var j=0;j<1;j++){ if ( i== 3) { break; } alert(i); } } 【只是3不弹出最外面没有停止】 有时候有需求把所有的循环都跳出 a : for(var i=0;i<5;i++){ for(var j=0;j<1;j++){ if ( i== 3) { break a; } alert(i); } } for循环其他写法 var i = 0; for(;;){ alert(i); if (++i>5) { break; } } 5、匿名函数自执行 (function(){ alert(1111); })(); //在函数前面不加小括号,加上位运算符都可以解决这个问题 ~ + +function(){ alert(111); }(); 6、创建对象可以省略括号 系统对象也有类似的形式 function Aaa(){ } var a1 = new Aaa; alert(a1); var arr = new Array; alert( arr.length);
3、js函数手记
//第一种定义方式 function fn1() { alert("fn1"); } //函数就是一个非常特殊的对象,是一个Function类的实例,其实在内存中存储的操作是通过一个键值对来存储的 alert(typeof fn1); //由于函数是一个对象,所以可以通过如下方式定义 //以下是通过函数的拷贝来完成赋值,两个引用并没有指向同一个对象 var fn2 = fn1; fn2(); fn1 = function() { alert("fnn1"); } /** * 函数虽然是一个对象,但是却和对象有一些区别,【对象是通过引用的指向完成对象的赋值的】,而【函数却是通过对象的拷贝来完成的】 * 所以fn1虽然变了,并不会影响fn2 */ fn2(); fn1(); /** * 对于对象而言,是通过引用的指向来完成赋值的,此时修改o1或者o2会将两个值都完成修改 */ var o1 = new Object(); var o2 = o1; o2.name = "Leon"; alert(o1.name); /**特别指出:函数的参数和调用没有关系,如果函数只有一个参数,但是却传入 * 了两个参数,仅仅只会匹配一个 * 所以在js中函数不存在重载 * 函数的length就表示该函数所期望的参数值 * apply的第二个参数是一个参数数组, call的第二个参数是通过参数列表来完成传递, 第一个参数均为指定的上下文 */
//第一种直接Object定义方式: /** * 在js中并不存在类,所以可以直接通过Object来创建对象 * 但是使用如下方式创建,带来最大的问题是,没有类的约束 * 无法实现对象的重复利用,并且没有一种约定,在操作时会带来问题 */ var person = new Object(); person.name = "Leon"; person.age = 33; person.say = function() { alert(this.name+","+this.age); } //第二种JSON方式 /** * json的意思就是javascript simple object notation * json就是js的对象,但是它省去了xml中标签,而是通过{}来完成对象的说明 */ var person = { name:"张三",//通过属性名:属性值来表示,不同的属性通过,来间隔 age:23, say:function() { alert(this.name+","+this.age); }//最后一个属性之后不能有, } person.say(); /** * 通过json依然可以创建对象数组,创建的方式和js的数组一样 */ var ps = [ {name:"Leon",age:22}, {name:"Ada",age:33} ]; for(var i=0;i<ps.length;i++) { alert(ps[i].name); } /** * 创建一组用户,用户的属性有 * name:string,age:int,friends:array * 把ps转换为json */ var ps = [ { name:"Leon", age:22, friends:["Ada","Alice"] }, { name:"John", age:33, friends:["Ada","Chris"] } ]; //第三种工厂方式 /** * 通过工厂的方式来创建Person对象 * 在createPerson中创建一个对象 * 然后为这个对象设置相应的属性和方法 * 之后返回这个对象 */ function createPerson(name,age) { var o = new Object(); o.name = name; o.age = age; o.say = function() { alert(this.name+","+this.age); } return o; } /** * 使用工厂的方式,虽然有效的解决了类的问题,但是依然存在另外一个问题 * 我们无法检测对象p1和p2的数据类型,最多检测其是一个Object. */ var p1 = createPerson("Leon",22); var p2 = createPerson("Ada",33); p1.say(); p2.say(); //第四种构造函数方式: /** * 通过构造函数的方式创建,和基于工厂的创建类似 * 最大的区别就是函数的名称就是类的名称,按照类的约定,第一个 * 字母大写。使用构造函数创建时,在函数内部是通过this关键字来 * 完成属性的定义 */ function Person(name,age) { this.name = name; this.age = age; //以下方式带来的问题是所有的对象都会为该行为分配空间 // this.say = function() { // alert(this.name+","+this.age); // } this.say = say; } /** * 将行为设置为全局的行为,如果将所有的方法都设计为全局函数的时候 * 这个函数就可以被window调用,此时就破坏对象的封装性 * 而且如果某个对象有大 量的方法,就会导致整个代码中充斥着大量的全局函数 * 这样将不利于开发 */ function say() { alert(this.name+","+this.age); } /* * 通过new Person来创建对象 */ var p1 = new Person("Leon",22); var p2 = new Person("Ada",32); p1.say(); p2.say(); /** * 使用构造函数的方式可以通过以下方式来检测 * 对象的类型 */ alert(p1 instanceof Person); /** * 使用构造函数创建所带来的第一个问题就是每一个对象中 * 都会存在一个方法的拷贝,如果对象的行为很多的话 * 空间的占用率就会大大增加 * 可以将函数放到全局变量中定义,这样可以让类中的行为指向 * 同一个函数 */ alert(p1.say==p2.say);
(1)、定义类的静态属性、方法:因为function是一个对象,因此可以:类名.方法|属性。
(2)、delete : 对象属性删除 【类|json方式】
1.delete删除对象中的属性 function fun(){ this.name = 'mm'; } var obj = new fun(); console.log(obj.name);//mm delete obj.name; console.log(obj.name); //undefined ---------------------------------------------- var json = { name: '...', fn:function(){...} }; delete json.name; //成功删除 delete json.fn; //成功删除 2.直接用delelte删除不了变量 var name = 'lily'; delete name; console.log(name); //lily 3.删除不了原型链中的变量 fun.prototype.age = 18; delete obj.age; console.log(obj.age) //18
var aJsonObj = {};
aJson._proto_ = Person.prototype; //重写, 这样仅仅继承了Person中原型上的方法
aJson._proto_ = new Person();
aJson._proto_.constructor = Person; //完全继承了Person
函数定义类的第5种方式【原型】:
图片中的源码如下:
<script type="text/javascript"> /** * 原型是js中非常特殊一个对象,当一个函数创建之后,会随之就产生一个原型对象 * 当通过这个函数的构造函数创建了一个具体的对象之后,在这个具体的对象中 * 就会有一个属性指向原型 */ //第一种状态 function Person(){ } //第二种状态 Person.prototype.name = "Leon"; Person.prototype.age = 23; Person.prototype.say = function() { alert(this.name+","+this.age); } //第三中状态,创建了一个对象之后会有一个_prop_的属性指向原型 //在使用时如果在对象内部没有找到属性会去原型中找,_prop_属性是隐藏的 var p1 = new Person(); //p1.say(); //以下方法可以检测出p1是否有_prop_指向Person的原型 //alert(Person.prototype.isPrototypeOf(p1)); //第四种状态 var p2 = new Person(); //是在自己的空间中定义了一个属性,不会替换原型中的属性 p2.name = "Ada"; //p2.say(); //p1.say(); // //检测某个对象是否是某个函数的原型 // alert(Person.prototype.isPrototypeOf(p2)); // //检测某个对象的constructor // alert(p1.constructor==Person); // //检测某个属性是否是自己的属性 // alert(p1.hasOwnProperty("name"));//false,p1自己的空间中没有值 // alert(p2.hasOwnProperty("name"));//true,p2在自己的空间中设置了name属性 //delete p2.name; //只能删除自己空间中的属性 //p2.say(); //alert(p2.hasOwnProperty("name"));//由于已经删除了,所以是false //检测某个对象在{【原型或者自己】中是否包含有某个属性},通过in检测 //alert("name" in p1);//true //alert("name" in p2);//true //alert("address" in p1);//在原型和自己的空间中都没有,false alert(hasPrototypeProperty(p1,"name"));//true alert(hasPrototypeProperty(p2,"name"));//false /** * 可以通过如下方法检测某个属性是否在原型中存在 */ function hasPrototypeProperty(obj,prop) { return ((!obj.hasOwnProperty(prop))&&(prop in obj)) } </script>
原型重写的问题:
图片中的源码如下:
<script type="text/javascript"> /** * 原型是js中非常特殊一个对象,当一个函数创建之后,会随之就产生一个原型对象 * 当通过这个函数的构造函数创建了一个具体的对象之后,在这个具体的对象中 * 就会有一个属性指向原型 */ //第一种状态 function Person(){ } var p1 = new Person(); Person.prototype.sayHi = function() { alert(this.name+":hi"); } /** *如果把重写放置在new Person之后,注意内存模型,原型重写的问题 */ Person.prototype = { constructor:Person,//手动指定constructor name:"Leon", age:23, say:function() { alert(this.name+","+this.age); } } p1.sayHi();//不会报错,但是没有this.name var p2 = new Person(); //p2.sayHi();//此时p2没有sayHi,所以就会报错 p2.say(); p1.say(); //此时p1没有say(),所以就会报错 </script>
原型弊端:
/**
*基于原型的创建虽然可以有效的完成封装,但是依然有一些问题。
* 1、无法通过构造函数来设置属性值。
* 2、当属性中有引用类型变量时,可能存在变量值写入。
*/
函数定义类的第6种方式【组合构造函数和原型方式】:
<script type="text/javascript"> /** * 为了解决原型所带来的问题,此处需要通过组合构造函数和原型来实现对象的创建 * 将属性在构造函数中定义,将方法在原型中定义 * 这种有效集合了两者的优点,是目前最为常用的一种方式 */ function Person(name,age,friends){ //属性在构造函数中定义 this.name = name; this.age = age; this.friends = friends; } Person.prototype = { constructor:Person, //方法在原型中定义 say:function() { alert(this.name+"["+this.friends+"]"); } } //此时所有的属性都是保存在自己的空间中的 var p1 = new Person("Leon",23,["Ada","Chris"]); p1.name = "John"; p1.friends.push("Mike");//为p1增加了一个朋友 p1.say(); //John[Ada,Chris,Mikes] var p2 = new Person("Ada",33,["Leon"]); p2.say();//Ada[leon] </script>
函数定义类的第7种方式【动态原型方式】:
<script type="text/javascript"> function Person(name,age,friends){ //属性在构造函数中定义 this.name = name; this.age = age; this.friends = friends; //不能使用重写的方式定义 /*Person.prototype = { constructor:Person, //方法在原型中定义 say:function() { alert(this.name+"["+this.friends+"]"); } }*/ /** * 判断Person.prototype.say是否存在,如果不存在就表示需要创建 * 当存在之后就不会在创建了 */ if(!Person.prototype.say) { Person.prototype.say = function() { alert(this.name+"["+this.friends+"]"); } } } //此时所有的属性都是保存在自己的空间中的 var p1 = new Person("Leon",23,["Ada","Chris"]); p1.name = "John"; p1.friends.push("Mike");//为p1增加了一个朋友 p1.say(); //John[Ada,Chris,Mikes] var p2 = new Person("Ada",33,["Leon"]); p2.say();//Ada[leon] </script>
4、js继承的几种方式定义:
【1-基于原型链的方式】对应的内存分布图:
<script type="text/javascript"> function Parent() { this.pv = "parent"; } Parent.prototype.pp = "ok"; Parent.prototype.showParentValue = function() { alert(this.pv); } function Child() { this.cv = "child"; } /** * 如果想进行赋值之后,才进行原型链的设定,这样赋值的原型对象 * 就会被重写掉,赋值的对象就不存在在新的原型对象中 */ // Child.prototype.showChildValue = function() { // alert(this.cv); // } /** * 让Child子类的原型链指向父类Parent对象,也就等于完成了一次继承 * 注意内存模型 */ Child.prototype = new Parent(); Child.prototype.showChildValue = function() { alert(this.cv); } /** * 此时完成的对父类对象的覆盖 */ Child.prototype.showParentValue = function() { alert("override parent"); } /** * 在使用原型链进行继承一定要注意一下问题: * 1、不能在设定了原型链之后,再重新为原型链赋值 * 2、一定要在原型链赋值(赋了父类的实例对象)之后才能添加或者覆盖方法 */ /** * 当执行了下面这句话之后,意味着Child的原型又重写了 * 这样就不存在任何的继承关系了 * 使用原型链需要注意的第一个问题 */ // Child.prototype = { // showChildValue:function() { // alert(this.v); // } // } var c = new Child(); c.showParentValue(); //override parent c.showChildValue(); //child alert(c.pp); //ok /** * 【原型链继承方式中规定不能对父类构造函数传递参数,因为面向对象中规定,属性隶属于对象所有】 * 第一个缺点:使用原型链继承,最大的缺点是,无法从子类中调用父类的构造函数 * 这样就没有办法把子类中的属性赋值到父类 * 第二个缺点:如果父类中有引用类型,此时这个引用类会添加到 * 子类的原型中,当第一个对象的修改了这个引用之后,其他对象的引用同时修改 * 所以一般都不会单纯的使用原型链来实现继承 */ var c1 = new Child(); //Child中的原型的color被修改 c1.color.push("blue"); alert(c1.color);//red,yellow blue var c2 = new Child(); alert(c2.color);//red yellow blue </script>
<script type="text/javascript"> function Parent(name) { this.color = ["red","blue"]; this.name = name; //this.say = function() { // alert(this.name); // } } /** * 由于使用伪造的方式,不会完成Child的原型指向Parent * 所以say方法不存在,解决方法是,将这个方法放置到 * Parent中使用this来创建,但是此时每个对象中又存在say * 这样空间占用太大,所以也不会单独的使用伪造的方式实现继承 */ // Person.prototype.say = function() { // alert(this.name); // } function Child(name,age) { this.age = age; //使用伪造的方式就可以把子类的构造函数参数传递到父类中 Parent.call(this,name); //在Child中的this明显应该是指向Child的对象 //当调用Parent方法的时候,而且this又是指向了Child //此时就等于在这里完成this.color = ["red","blue"] //也就等于在Child中有了this.color属性,这样也就变相的完成了继承 //Parent.call(this); //这种调用方式,就仅仅完成了函数的调用【此时的上下文this-->window】,根本无法实现继承 //Parent(); } var c1 = new Child("Leon",12); var c2 = new Child("Ada",22); alert(c1.name+","+c1.age); alert(c2.name+","+c2.age); </script>
【3-基于原型、伪造组合的方式】对应的内存分布图:
<script type="text/javascript"> function Parent(name) { this.color = ["red","blue"]; this.name = name; } Parent.prototype.ps = function() { alert(this.name+"["+this.color+"]"); } function Child(name,age) { Parent.call(this,name); //伪造方式继承属性 this.age = age; } Child.prototype = new Parent(); //原型方式继承行为 Child.prototype.say = function() { alert(this.name+","+this.age+"["+this.color+"]"); } var c1 = new Child("Leon",22); c1.color.push("green"); c1.say(); //lenon,22[red,blue,green] c1.ps(); //lenon[red,blue,green] var c2 = new Child("Ada",23); c2.say();//ada,23[red,blue] c2.ps();//ada[red,blue] </script>
5、js函数作用域链
6、js闭包