第2章--进阶篇
类型进阶
类型:
Undefined
Null
Boolean
String
Number
Object
原始类型(值类型):undefined, null, true, "...", 123
对象类型(引用类型):var obj = {}; var arr = []; var date = new Date();
JS的三大对象类型:
原生对象:
JS语言规范(ECMA规范)所定义的一系列对象。
可分为两部分:
构造函数:用开发者定义的内置构造函数来定义一系列对象
由Boolean/ String/ Number/ Object/ Function/ Array/ Date/ RegExp/ Error组成
对象:Math, JSON, 全局对象,arguments
宿主对象:
浏览器运行环境提供的一系列对象,包括window, document, history, navigator等
这些对象提供了诸如dom操作等的API,开发者可操作网络节点等。(在DOM课程详解)
浏览器扩展对象:各浏览器厂商为自己的浏览器所扩展的浏览器对象
早期--为不同浏览器开发不同页面--兼容性
规范的发展--浏览器扩展对象慢慢被弃用--了解即可
原始类型和对象类型的区别:
问:如何复制一个对象?如何克隆出一个独立但属性、方法完全一样的对象
https://www.zhihu.com/question/23031215
隐式类型转换:(弱类型语言)
什么时候发生隐式类型转换呢:
1. 数字运算符
i.e.
10 + <input type="text" id="num"/> <input type="button" value="等于" id="btn"/> <span id="ret"></span> <script type="text/javascript"> var btn = document.getElementById("btn"); var ret = document.getElementById("ret"); var num = document.getElementById("num"); btn.addEventListener('click', function() { ret.innerText = 10 + num.value; }) </script>
此时会将10隐式转换成String后进行+的字符串连接操作 // 1010
若是ret.innerText = 10 - num.value; // num.value字符串会被隐式转换成数字后进行算术运算
总结:除了+运算外,当一方是数字、另一方为字符串时,会被隐式转换为数字进行计算
当为+运算时,会被隐式转换为字符串进行拼接
2. . 号
将直接量隐式转换成对象后即可调用该对象的方法。
i.e.
(3.1415).toFixed(2); // "3.14"
console.dir(3.1415); // undefined
系统在进行 . 操作时,进行了隐式类型转换成对象类型(该例为一个Number对象)。
"hello world".split(" "); // 将"hello world"直接量隐式转换成了String对象。
3. if语句
条件语句的条件会被隐式转换成boolean类型的值
4. ==
隐式类型转换结果:
显式类型转换
以上例为例子:ret.innerText = 10 + Number(num.value); // 20
Number(), String(), Boolean()
parseInt(), parseFloat() // 取数值
!, !! // 取布尔值
类型识别:
什么时候需要使用到类型识别呢?
i.e. function toDate(param) {
// 输入格式: '2015-08-05'/ 1438744815232/ {y:2015, m:8, d:5}/ [2015,8,5]
// 返回格式:Date
function toDate(param) { if (typeof(param) == 'string' || typeof(param) == 'number') { return new Date(param); } if (param instanceof Array) { var date = new Date(0); date.setYear(param[0]); ... return date; } if (typeof(param) == 'object') { var date = new Date(0); date.setYear(param.y); ... return date; } return -1; }
类型识别的方法:
typeof(...) / typeof ...
typeof "jerry"; // "string"
typeof 12; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" X
typeof {name:"jerry"}; // "object"
typeof function() {}; // "function"
typeof []; // "object"
typeof new Date; // "object"
typeof /\d/; // "object"
function Person() {}; typeof new Person; // "object"
总结:typeof可以识别标准类型(Null除外);
但是不能准确识别具体的对象类型(function可以)
instanceof
[] instanceof Array; // true
/\d/ instanceof RegExp; // true
1 instanceof Number; // false
"jerry" instanceof String; // false
function Point(x,y) { ... }
function Circle(x, y, r) { ... }
var c = new Circle(1,1,2);
c instanceof Circle // ture
c instanceof Point; // true
总结:可以判别内置对象类型,但是不能判别原始类型;
可以判别自定义对象类型(包括父类子类)
--> 可以判别所有的对象类型
Object.prototype.toString.call
Object.prototype.toString.call(123); // "[object Number]"
Object.prototype.toString.call("123"); // "[object String]"
Object.prototype.toString.call(function() {}); // "[object Function]"
function Point(x,y){ ... }; Object.prototype.toString.call(new Point(1,2)); // "[object Object]"
总结:可以识别标准类型,可以识别内置对象类型
但是不能识别自定义对象类型
constructor
constructor为构造对象的构造函数本身
当创建一个对象后,会发现对象有一个属性称为constructor
"jerry".constructor === String; // true (.运算符会隐式转换成对象类型)
(1).constructor === Number; // true
({}).constructor === Object; // true
new Date().constructor === Date; // true
[].constructor === Array;
function Person(name) {...}
new Person("jerry").constructor === Person; // true
总结:可以判别标准类型(Undefined/Null除外,因为这两个类型没有构造函数)
可以判别内置对象类型
可以判别自定义对象类型
--> 利用constructor自己写类型判断函数:
function getConstructorName(obj) { return (obj===undefined||obj===null)?obj:(obj.constructor&&obj.constructor.toString().match(/function\s*([^(]*)/)[1]); }
getConstructorName(undefined); // undefined
getConstructorName(null); // null
getConstructorName(new Date()); // "Date"
函数进阶
函数定义的方法:
1. 函数声明:function name() {}
2. 函数表达式:var name = function() {};
3. 函数实例化(少用不推荐):var name = new Function("..", "..", "return ..");
三种函数定义方法的区别:
定义与调用的顺序:
用声明方式定义的函数,可以在函数被声明之前就调用。
因为JS执行过程:JS虽然是脚本语言,但执行顺序不是简单的逐行执行:
1. 预解析:变量声明、函数定义;2. 单步执行JS代码
函数表达式和函数实例化方式定义的函数,不能在定义前被调用。
重复定义:
函数声明:最后声明的为有效声明。
函数表达式和函数实例化:按定义和调用顺序执行。
若:同一个函数被函数声明和其他方法均定义:
其他方法的定义会覆盖函数声明方式的定义。
总结:
函数声明的特点:函数定义会被前置(预解析);重复定义时以最后一次定义为有效定义。
函数实例化的特点:(由于作用域的关系)除了函数内部作用域外,不会逐级向上而是直接到全局作用域。
--该函数只能访问本地作用域和全局作用域。
var person = {name:"aaa", age:50}; (function() { var person = {name:"aaa", age:30}; var func = new Function("console.log(person.age);"); func(); }) ();
此时返回的为age=50; 而不是30.
var func = new Function("var person = {name:"aaa", age:10}; console.log(person.age););})();
则会返回age=10,因为是函数内作用域
函数调用的方法:
1. 函数调用模式:func_name(param);
2. 方法调用模式:
var var_name = {
...,
func_name: function(param) { ... }
}
var_name.func_name(args);
3. 构造函数调用模式:new Function("");
4. apply调用模式:
JS中的函数为对象,任何函数中均有apply()方法。
Function.prototype.apply的使用:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.move = function(x, y) { this.x += x; this.y += y; } var p = new Point(0, 0); p.move(2, 2); // 方法调用模式 var circle = {x:1, y:1, r:1}; // 直接定义一个对象 // 如何实现将circle对象进行move()操作呢 p.move.apply(circle, [2, 1]); // 将p的move的方法借用给circle使用
函数调用模式的区别-this:
在函数调用的时候,会自动添加this和arguments这两个临时变量
this的指向区别:
函数调用模式:this指向全局对象Window(内部嵌套的函数中的this也指向Window)
方法调用模式:this指向该对象(调用者)
构造函数调用模式:this指向被构造的对象
apply (call)调用模式:将this的指向从原对象变为函数中的第一个参数对象(上例中为circle对象)
Arguments:
是类似数组的对象(Array-like)
arguments[index]
arguments.length
函数传参:
原始类型变量按值传递 - call by value
对象类型变量按引用传递 - call by reference???
看起来是的,但是:实际上是按共享传递 - call by sharing
传递时,获取了对象地址的副本,因此指向的是同一个对象,而当在函数内把该变量指向的地址改变了,也并不影响原来的对象。
var count = {a:1, b:1}; var addOne = function(obj) { obj = {a:2, b:2}; return obj; } var ret = addOne(count); console.log(ret); // Object {a:2, b:2} console.log(count); // Object {a:1, b:1}
闭包:Closure
函数内部定义的子函数,用到了父函数的变量形成的这一个特定作用域
i.e.
(function() { var a = 1; (function() { console.log(a); debugger; })() })();
debugger时,在Scope中:
Local: this: Window
Closure: a: 1
有哪些功能:
1. 保存函数的执行状态
/* 将字符串中的一些特定字符按顺序用数组中的元素替换,如 var arr = ['c', 'f', 'h', 'o']; var str = ‘ab4de8g4ijklmn7'; 替换后为 'abcdefghijklmno' */ // 思路:让函数记住自身被调用的次数 var func = (function(){ var count = 0; return function(){ return arr[count++]; } })(); str = str.replace(/\d/g, func);
2. 封装
不让对象使用者直接access/modify某些属性 -- 类似于private
var Car = function(type) { var status = "stop", return { type: type, start: function() { status = "driving"; }, stop: function() { status = "stop"; }, getStatus: function() { console.log(status); } } } var audi = new Car("audi");
此时在外部访问audi,则不能直接访问status的值。
3. 性能优化
(减少函数定义时间和内存消耗)
// 不使用闭包 function sum(i, j) { var add = function(i, j) { return i+j; } } var startTime = new Date(); for(var i = 0; i < 1000000; i++) { sum(1,1); } var endTime = new Date(); console.log(endTime - startTime); // 195 milliseconds
每次调用sum()时都需要在函数内部定义一个add函数
// 使用闭包 var sum = (function() { var add = function(i, j) { return i+j; } return function(i,j) { add(i,j); } })() var startTime = new Date(); for(var i = 0; i < 1000000; i++) { sum(1,1); } var endTime = new Date(); console.log(endTime - startTime); // 17 milliseconds
add()为sum的闭包作用域里的函数,不需要每次都定义,节约了时间
-- 可以将不需要保存状态的频繁调用的函数放入闭包作用域内以优化性能(基于js执行性能考虑,被频繁调用的函数内部定义和调用的帮助函数,如果不需要保存状态,应该将这些帮助函数保存到闭包作用域。)
First-class function:函数可被当做普通变量使用(比如可被作为函数的参数或函数的返回值)
功能:
将函数作为参数:比如.forEach(...)/ .replace(...)/ 异步回调函数 等
将函数作为返回值:Function.prototype.bind (与apply类似)
上例中的Point和circle例子:p.move.apply(circle, [2, 1]);
可使用bind:var circlemove = p.move.bind(circle, 2, 1); 返回的是函数的引用
区别:代码执行完circle不会立即移动,需调用circlemove();来完成移动
NB: 可以绑定为var circlemove = p.move.bind(circle, 1); 调用时circlemove(1);
或绑定为var circlemove = p.move.bind(circle); 调用时circlemove(2, 1);
curry:函数克里化:严格意义:将接受多个参数的函数转化为一个接受单一参数并返回一个接受余下参数并返回结果的新函数的函数的技术
var sum = function(a, b, c) { return a+b+c; } --> var sum_curry = function(a) { return function(b, c) { return a+b+c; } }
更泛化的定义:给函数分步传递参数, 每次函数接受部分参数后应用这些参数,并返回一个函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数的函数,直至返回最后结果。归纳为逐步传参,逐步缩小函数的适用范围,逐步求解的过程。
// currying实现将一个函数转变为柯里化函数 var currying = function (fn) { var _args = []; return function () { if (arguments.length === 0) { // 实现最终的计算 return fn.apply(this, _args); } // 这里只是简单的将参数缓存起来(用于解释柯里化概念,并非实际应用场景) Array.prototype.push.apply(_args, [].slice.call(arguments)); return arguments.callee; } }; // sum函数接受任意参数,并返回求和结果 var sum=function () { var total = 0; for (var i = 0, c; c = arguments[i++];) { total += c; } return total; }; // 或得一个泛化柯里化的sum函数 var sum_curry = currying(sum); sum_curry(1)(2,3); sum_curry(4); console.log(sum_curry());
从更上层的角度去理解,柯里化允许和鼓励你将一个复杂过程分割成一个个更小的更容易分析的过程(这些小的逻辑单元将更容易被理解和测试),最后这样一个难于理解复杂的过程将变成一个个小的逻辑简单的过程的组合。
上面两个例子很好的解释了什么是函数柯里化,但是什么时候用?任何能简化逻辑实现、提高可读性地方都鼓励使用
i.e.
// refresh函数实现通过ajax请求更新页面上的相关模块的数据。 function refresh(url, callback){ // ajax_get实现一个ajax get请求,请求成功后回调callback函数(这里不提供ajax_get实现,有兴趣的同学可以参考前面课程中有提到过类似的实现)。 ajax_get(url, callback); } function update(data){ // 更新的逻辑全部在这里处理 } refresh("xxx?target=news", update); // update函数是一个柯里化后函数,第一级函数根据传入参数将需要更新的模块分拆出来。 function update(target){ var _elm = document.getElementById(target); // 这里实现模块分拆,代码仅是举例 return function(data){ // 返回一个用请求结果更新页面显示的函数 _elm.innerHTML = data; // 这里实现用ajax请求返回结果更新页面显示过程,代码仅是举例 } } // 更新页面可以写成这样 refresh("xxx?target=news", update("news")); refresh("xxx?target=pictures", update("pictures")); // 继续,如果新闻模块需要继续拆分成“社会”新闻,“娱乐”新闻,那我们柯里化的update函数该怎么写呢?可以这样写: function update(target){ var _elm = document.getElementById(target); // 这里实现第一级模块分拆,代码仅是举例 return function(type){ // 返回一个接受其余参数的函数 var _elm = document.getElementById(item); // 这里实现第二级模块分拆,代码仅是举例 return function(data){ // 返回一个接受其余参数并最终更新页面显示的函数 _elm.data = data; // 这里实现用ajax请求返回结果更新页面显示过程,代码仅是举例 } } } // 更新页面就可以写成这样 refresh("action.do?target=news&type=society", update("news")("society")); refresh("action.do?target=news&type=entertainment", update("news")("entertainment"));
思考题1:函数声明和函数表达式定义同一个函数时,执行的是哪个?
// 以下代码执行时,三次打印分别输出什么?为什么? function add1(i){ console.log("函数声明:"+(i+1)); } add1(1); var add1 = function(i){ console.log("函数表达式:"+(i+10)); } add1(1); function add1(i) { console.log("函数声明:"+(i+100)); } add1(1);
101,11,11
思考题2:对象方法中定义的子函数,子函数执行时this指向哪里?
-
以下代码中打印的this是个什么对象?
-
这段代码能否实现使myNumber.value加1的功能?
-
在不放弃helper函数的前提下,有哪些修改方法可以实现正确的功能?
var myNumber = { value: 1, add: function(i){ var helper = function(i){ console.log(this); this.value += i; } helper(i); } } myNumber.add(1);
1. this为helper对象。2. 不能。
3.
//使用闭包 var myNumber={ value:1, add:function(i){ var that=this, helper=function(i){ console.log(that); that.value+=i; } helper(i); } } myNumber.add(1); //使用apply或call var myNumber={ value:1, add:function(i){ var helper=function(i){ console.log(this); this.value+=i; }, helper.apply(myNumber,[i]); } } myNumber.add(1); //使用方法调用 var myNumber={ value:1, helper:function(i){ console.log(this); this.value+=i; }, add:function(i){ this.helper(i); } } myNumber.add(1);
函数回顾:
函数定义3种方法--区别
函数调用4种模式--本地作用域,this, arguments
闭包和闭包的三个主要功能(保存函数状态、封装、性能优化)
First-class function(函数可作为参数或返回值)
函数的克里化实现 (通过first-class function)
原型
原型是什么:
类是具体事物的抽象--类构造对象:抽象->具体
原型是具体的,用一个现成的对象为原型,去构造新的对象:具体->具体
设置对象的原型:
1. Object.create(proto [, propertiesObject])
传入一个原型对象proto(propertiesObject为新对象的属性定义),返回一个新对象
i.e.
// 定义原型对象 var landRover = { name: 'landRover', start: function() { console.log(this.logo + "start"); }; run: function() { console.log(this.logo + "run"); }; stop: function() { console.log(this.logo + "stop"); }; } // 使用原型创建新的对象 var landWind = Object.create(landRover); landWind.logo = 'landWind'; var landCruiser = Object.create(landRover); landCruiser.logo = 'landCruiser'; // 启动 landWind.start();
创建新对象时,landWind/landCruiser中的_proto_指针指向landRover对象(_proto_属性指向原型,不能被直接修改)
新对象可以共享原型的属性和方法。
2. 构造函数:使用prototype设置原型
i.e.
// Car构造函数 function Car(logo) { this.logo = logo || 'unknown name'; } // 设置Car的prototype属性 Car.prototype = { start: function() { console.log(this.logo + "start"); }; run: function() { console.log(this.logo + "run"); }; stop: function() { console.log(this.logo + "stop"); }; } // 使用原型创建新的对象 var landWind = new Car('landWind'); var landCruiser = new Car('landCruiser'); // 启动 landWind.start();
创建新对象时(landWind),设置对象的原型,_proto_为构造函数的prototype属性,
将新对象(landWind)作为this去执行构造函数(Car),Car.apply(landRover, arguments);
原型链:
i.e.
// 在之前Car的构造函数和Car的prototype属性的基础上 // LandRover的构造函数 function Landrover(serialno) { this.serialNumber = serialno; } // 设置LandRover的prototype属性 LandRover.prototype = new Car('landRover'); // 创建LandRover对象 var landRover1 = new LandRover(10000); var landRover2 = new LandRover(10001); console.log(landRover1.serialNumber);
1. 定义Car和其prototype
2. 定义LandRover并设置其prototype=new Car('landRover'); 当通过new Car()创建对象时,会有原型指针指向Car.prototype
3. 使用构造函数LandRover创建对象landRover1,使用new LandRover()时,会有原型指针指向LandRover.prototype
4. 事实上,Car.prototype (是一个普通的对象)是通过new Object()创建出来的,会有原型指针指向Object.prototype
5. 从landRover1开始landRover1._proto_->new Car('landRover')._proto_->Car.prototype._proto_->Object.prototype._proto_:原型链
另一方面,(构造)函数本身也是对象,则:
landRover/Car中的_proto_都会指向Function.prototype
而Function.prototype可以通过new Object创建,于是同样的,Function.prototype._proto_->Object.prototype
对象的访问、修改、删除都跟原型链有关
访问对象指针时首先会在对象本身查找,若没有,会随着原型链往上查找。
i.e. console.log(landRover1.serialno); // 在对象landRover1中直接查找到
console.log(landRover1.toString()); // 随着原型链往上一个一个查找,直到Object.prototype中查找到toString()方法
修改和删除只能作用于对象自身的属性(若属性在原型,则会在自身创建该属性并赋值,不会改变原型)
hasOwnProperty():
每一个对象都拥有hasOwnProperty()(来自Object.prototype)
判断传入的属性是不是对象自身的属性
i.e. landRover1.hasOwnProperty('serialno'); // true
landRover1.hasOwnProperty('logo'); // false
作业:
编码实现下面删除数组中重复元素的功能
[2,4,2,3,4].deleteRepeat() 返回:[2,4,3]
变量作用域
静态作用域:
又称为词法作用域
由程序定义的位置决定,和代码执行顺序无关
var x = 10; function foo() { alert(x); } function bar() { var x = 20; foo(); } bar();
该例中,若是静态作用域(只跟程序定义的位置有关):
1. 全局作用域:x=10; foo=<function>; bar=<function>
2. foo和bar在全局作用域中,bar的作用域:x=20;
3. 即使foo函数内没有变量x,但是foo函数会在外面(即全局作用域)找到x=10;
动态作用域:
在程序运行时刻决定,使用动态栈(压栈、从栈顶取出)
上例中,若是动态作用域:
1. x=10压入栈,紧接着定义foo:<function>压入栈,最后定义bar:<function>压入栈(此时x=10在最底端)
2. 之后执行bar函数,在bar函数内定义了x=20,将x=20压入栈(此时x=20在最顶端)
3. 紧接着调用了foo函数,foo函数中alert(x); 在栈中查找x的值,离栈顶最近的即为x=20
JS变量作用域:
使用静态作用域
没有块级作用域(比如if/while/for等语句的大括号语句块)
一共有两种作用域:全局作用域、函数作用域
ES5中使用词法环境来管理静态作用域
词法环境:描述环境的对象
包含两部分:
1. 环境记录:用来记录环境里面定义的形参、函数声明以及变量等。
2. 对外部词法环境的引用(outer)
var x = 10; function foo(y) { var z = 30; function bar(q) { return x + y + z + q; } return bar; } var bar = foo(20); bar(40);
最外层创建了全局环境(全局环境也有一个outer引用,值为null)
之后的foo函数创建了foo环境(由于foo是在全局环境中定义的,所以foo函数有一个outer引用指向全局环境)
之后的bar函数创建了bar环境,有一个outer引用指向foo环境
环境记录初始化:(即声明提前)
在每一块代码执行前,会初始化改代码的环境
上例中,
先创建全局环境, outer:null,
全局环境的环境记录:
1. 函数声明(没有形参,跳过第一步形参):foo: <function>
2. 变量:x: undefined; bar: undefined
创建好环境记录后,开始执行全局环境中的代码:
x=10;
bar=foo(20);
此时准备执行foo函数
foo函数执行前,创建foo environment。先扫描整个函数中的内容,将形参、函数声明和变量定义在环境记录中。
foo的环境记录:
1. 形参:y: 20;
2. 函数声明:bar: <function> {formalParameter:..., functionBody:..., scope:指向foo环境(用于之后将outer赋值为scope)
(函数声明和函数表达式的区别:函数声明是提前创建的,函数表达式是执行到该语句的时候才创建的)
3. var定义的变量:z: undefind(此时初始化为undefined而不是30)
创建好foo的词法环境后,开始执行foo函数
z=30;
return bar;
bar(40); 此时准备执行bar函数
创建bar的词法环境:outer引用指向foo的词法环境
1. q=40
2. 没有函数定义
3. 没有var变量
开始执行bar函数
return x + y + z + q; // 这些变量都需要在bar的环境记录里面找。
x: bar:outer->foo:outer->global environment: x=10;
y: bar:outer->foo: y=20
z: bar:outer->foo: z=30
q: bar: q=40
特殊的词法环境:
with:
var foo = "abc"; with({ foo: "bar" }) { function f() { alert(foo); }; (function() { alert(foo); })(); f(); }
全局环境:
outer: null;
f:<function>;
foo:"abc"; with()语句
with语句会创建一个临时的词法环境,将传入的对象中的属性定义到with的词法环境记录中。
(函数声明或var变量定义在with块内或with块外是没有差别的)
with environment:
outer:global;
foo:"bar";
之后为function f(){}定义,跳过
匿名函数创建词法环境Anonymous environment:outer:with environment;
alert(foo); --Anonymous:outer->with:foo="bar";
之后准备执行f();
创建f函数的词法环境:
outer:global:不是with环境。f函数是定义在全局环境内的,所以f函数的scope是全局环境。当f函数开始执行时,它的outer即为global
alert(foo): f:outer->global:foo="abc";
// 如果是函数表达式,则会看是在with内执行的,outer:with env;而函数声明是不管这些的,取决于什么时候定义。
try-catch:
try { var e = 10; throw new Error(); } catch (e) { function f() { alert(e); } (function() { alert(e); })(); f(); }
闭包
面向对象
JS进阶篇的单元测验:
http://www.jianshu.com/p/984e7dc3afbe
JS进阶篇的单元作业:
http://www.jianshu.com/p/30274a76ac8d