JavaScript Oriented
探究面向对象的JavaScript高级语言特性
Prologue . JavaScript Introduce
1. JS
Abstract
JavaScript是由Netscape公司工程师Brendan Eich研发的脚本语言,经过推广和流行,兼容ECMA-262标准,至今用于描述HTML网页行为。(前端验证,检测,响应,触发,控制等动态行为)
Knowledge Tree
2. About Document
本文涉及到的概念有JavaScript概述,对象类型系统,原型链,作用域链以及上下文this,闭包,命名空间以及面向对象的高级语言特性应用。
JavaScript知识点庞杂且个人能力和学习时间有限,希望得到有心者更多的鼓励与启发。
Chapter one . Type Family
1. That’s all Object things?
从面向对象角度理解,JS确实一切皆对象。但是JS是函数式编程,从面向过程角度可以理解为一切皆函数,这可能是JavaScript魅力所在。本文叫做面向JavaScript,偏向从面向对象角度理解。
如何理解“一切皆对象”。一般来说我们会从对象构造器和原型链解释,本文后文中有详细概述,这里不作初步探讨。
2. Begin from GLOBAL
这里我们从JS最特殊的内置对象GLOBAL开始。
介绍 |
GLOBAL是ECMAScript5规范中两个内置对象其中之一,表示全局对象。 |
||
作用 |
任何不属于JavaScript其他对象的属性和方法都属于GLOBAL。 |
||
实现 |
JavaScript对其并没有明确的实现。浏览器将GLOBAL作为宿主对象Window的一部分实现。 JSEngine的起始阶段就是实例化一个Window对象。 这也解释了(this === window) == true; |
||
属性 |
预定义对象 |
Object Array Function Boolean String Number Date RegExp Error EvalError RangeError ReferenceError SyntaxError TypeError URIError(作为函数原型的构造器) |
|
全局属性 |
undefined NaN Infinity … |
||
全局函数 |
编解码 |
decodeURI()/decodeURIComponent()/encodeURI()/ encodeURIComponent()/escape()/unescape() |
|
转换 |
getClass()/Number()/String()/parseFloat()/parseInt() |
||
判断 |
isFinite()/isNaN() |
||
执行 |
eval() |
||
用户定义 |
如var _temp_val = {}; // window._temp_val = {};全局变量混乱问题 |
||
Tips:全局属性可以直接使用而不用[window. ] Object,同时除了用户定义以为的属性不能delete |
在JS引擎启动并实例化window对象时,JS原生对象和浏览器对象全部作为预定义对象放置在window中。
当我们需要新建对象时,首先对象原型将会依据这些预定义对象被构建。
举个小栗砸:(这里有一些构造器和原型链的应用,对后面章节的梳理有帮助。)
var window.namespaceA = [];//声明命名空间
namespaceA.A = {…};//声明函数,JS引擎加载时将A.prototype丢到堆内存
var a = Object.create(A.prototype,{ …args });
JS引擎行为:
Object() = window.Object.prototype.constructor;//Object构造函数
Function() = window.Function.prototype.constructor;//Function构造函数
A() = window.namespaceA.A.prototype.constructor; // A构造函数
a = new A({ …args});//以A对象为原型实例化引用a
a.[[prototype]] = A.prototype;
3. We Are Family
、
该图总结并参考了ECMAScript5规范,如有错误请指出。
部分对象请参考W3C教程:http://www.w3school.com.cn/jsref/index.asp
Chapter two . User Object
这一篇章进入对象的讲解,讲述在当前JS执行环境executable code(ECStack)中,如何创建一个原型实例对象。分配内存,形成作用域链与原型链。改变执行控制权并返回对象。
总结:this new a object by prototype chain in ECStack, then this got return and leave ECStack.
1. Context – ECStack
JS引擎执行过程是JS对象的生命周期的交替的过程,JS引擎解析执行。当执行子函数时,会将引擎操作的控制权让给子函数。子函数本身就是一个函数上下文。
window.ECStack = [];//模拟执行环境,实际的执行环境包含window全局上下文
备注:与执行上下文相关的作用域和参数对象在new小节讲解。
执行环境ECStack内存时序结构图如图所示。
2. WHAT – object
ECMAScript5规范丰富了用户自定义Object对象,提供了Object对象的属性特性。
参考W3C教程:http://www.w3school.com.cn/js/pro_js_referencetypes.asp
备注: JSON对象拓展,使用JSON. Stringify(/*Object*/)和JSON.parse(/*String*/)进行对象序列化,有时依赖引用对象的toJSON()方法。
3. TYPE – prototype chain
JS是元解释型语言,当JS引擎执行JS代码时,会分析语法结构,并将得到的对象结构放置在堆内存中,称为原型。内存中的所有对象都包含一个属性[[prototype]]指针指向堆内存的原型,FireFox,Chrome等浏览器将该属性定义为__proto__可显示调用。
酱:Foo.__proto__ à Foo.prototype
同时原型对象存在原型,形成一条原型链。当JS引擎实例化window对象构造全局上下文this时,堆内存中生成如下原型链:
图中每一个矩形都是一个原型对象。每个原型对象都有自己的构造器函数。
酱:Foo.prototype.constructor == Foo();//注意这里是函数,只是JS允许写成Foo。
我将JAVA与JavaScript做类比,虽然这样可能不太妥当。
JavaScript概念 |
Java概念 |
预定义对象,如Object |
API对象,如java.lang.Object |
内核 |
JVM |
内核初始化window时,原型对象Object.prototype加载到堆内存中 |
JVM启动后,类信息Object.class加载到方法区中。 |
Object()//Object.prototype. constructor构造函数 |
Object构造函数 |
var obj = new Object(); |
Object obj = new Object(); |
obj.__proto__ == Object.prototype |
obj.getClass() == Object.class; |
Constructor构造函数概念是一个动态概念,意味着运行时内核中的原型对象才包含构造函数。理解预定义对象,原型对象,原型对象构造函数和实例化对象后,需要引申一个概念:如何判定对象类型。
关键字 |
类型 |
返回值 |
说明 |
typeof |
一元运算符 |
字符串 |
基本类型undefined string number Boolean 引用类型object 函数类型 function。 不返回null的原因:object JS类型值是存在32 BIT 单元里,32位有1-3位表示TYPE TAG,其它位表示真实值。object的标记位为000。而null标记位为00,最终体现的类型还是object.. |
instanceof |
二元运算符 |
布尔值 |
判断除undefined null一个变量是否某个对象的实例,就是看该对象是否在该变量的原型链上。 |
constructor |
函数 |
构造函数 |
var temp = obj.constructor.toString(); temp.replace(/^function (\w+)\(\).+$/,‘$1‘); |
prototype |
对象 |
原型类型 |
Object.prototype.toString.call(obj).slice(8,-1).toLowerCase(); |
这时候我们可以把本节原型链内存结构图补齐啦:
当查询对象的属性时,若该对象不存在该属性,则在该对象的原型链中向上查找对象原型是否存在该属性。若存在,则返回该属性,若直到原型为null时仍未找到该属性,返回undefined。
javascript找属性就是找最近的,找不着就找他爹。
4. WHO – this
this是当前执行环境上下文的一个属性。JavaScript是单线程的,意味着JS内核执行JS代码切换上下文,this就会改变。若JS执行到代码片段A,说明包含片段A的上一级内容B正在被加载/实例化,那么B就是this。
ECStack = { VO : {…}, this : thisValue};
初始状态
当浏览器加载请求页面时,卸载原始事件,解析并渲染HTML,驱动当前事件并绑定当前窗口对象。每个窗口都有自己的全局对象,当页面被加载后,window对象实例化并绑定this。
function main(){
window = new Window();
this.call(window);//扩展this环境对象,其实只有函数对象才有call方法
|
无状态
这一块并没有过多的代码演示。个人有两点理解。
- A调用方法a,则a方法内的this为A。
- 若A中没有a方法,则a方法内的this为A原型链上包含方法a最近的原型对象B。
function Foo () {}
Foo.prototype.foo = "bar";
Foo.prototype.logFoo = function () { eval("console.log(this.foo)");
//logs "bar" }
var foo = new Foo ();
foo.logFoo();//foo调用logFoo方法,只有foo原型有logFoo方法,所以Foo.prototype为this
备注:判断a是否是A的自由属性,使用hasOwnProperty方法。
备注:可以使用with,apply,call,bind方法连接this环境上下文,实现属性继承,这里不作过多说明。
5.
DO – new
Introduce
new关键词实例化一个对象。对象实例化过程,对this的不断赋值,构造参数对象arguments,并维护实例对象的constructor属性。
Step
举个栗砸。使用构造函数自定义对象:
1 function A(/* number*/ id, /*
string*/name){ this.id = id; this.name = name;}
2 var a = new A(1);//a.__proto__ =
A.prototype
分解步骤如下:
- 执行1行时,加载A对象构造函数并放入内存,构造原型链
A.prototype.constructor=
A(){…};
A() = new Function();
Function() = new
Object();
- 执行2行时,使用构造函数实例化a,并将A.prototype作用域给实例a
Var a = new A();
A.call(a, _args);//_args.id
=1;
- 执行内存中构造函数A.prototype.constructor前,构造参数对象arguments对象
arguments = A.prototype.slice.call(_args);;//传入参数
arguments.callee ==
this;//参数对象调用者为当前函数体
备注:arguments.length表示实际传参数,arguments.callee.length表示期望传参数
- 执行内存中构造函数A.prototype.constructor时,根据参数赋值,继承原型链中的属性方法。
a.id = arguments[1];//值为1
a.name = arguments[2];//值为undefined
a.constructor = A.prototype.constructor
= A(){…};//继承
- 返回值
return a;
Ways
新建对象方法 |
描述 |
备注 |
使用JS内存中内置对象的构造方法 |
var str = new String(“str”); |
只能实例化内置对象 |
使用JSON字面量 |
var o = {‘name’ :‘sapphire’}; |
方便快捷的方式 |
工厂模式 |
自定义函数中创建对象并返回 |
抽象方式 |
构造函数模式 |
function Foo(_args){…}; var foo = new |
常用方式 方法无法共享 |
原型模式 |
function Foo(id){Foo.prototype.id = id}; |
切断已存在实例与原型对象关系 属性共享 |
组合模式 |
function Foo(){…}; Foo.prototype = {fun |
构造函数模式构造属性 原型模式构造方法 |
6.
RESULT – return
当构造函数无特殊声明return返回值时,返回实例化对象。
Chapter three . closure
函数闭包是一种技术,尤其被Lambdas语言广泛应用。Java中可以使用匿名函数的方式模拟闭包。
1.
Introduce
闭包 = 闭包函数 + 一组自由变量与名称绑定存储区映射,实现函数外部访问函数内部变量的技术。
原理:如果一个函数包含内部函数,那么它们都可以看到其中声明的变量;这些变量被称为‘自由’变量。然而,这些变量可以被内部函数捕获,从高阶函数中return实现‘越狱’,以供以后使用。唯一需要注意的是,捕获函数必须在外部函数内定义。函数内没有任何局部声明之前(既不是被传入,也不是局部声明)使用的变量就是被捕获的变量。
备注:Javascript中20%以上的博客是关于闭包概念的,这里不再赘述。
弊端:应用时需分析函数变量,检查闭包是否会互相产生干扰,检查闭包的实例是否相同。
利端:闭包函数实例化时只执行一次,将不需要暴露在外层环境的变量封装在内部,减少了外部变量。
Chapter four . inherit
在ECMAScript中,只支持实现继承而不支持签名继承(接口继承)实现继承基本是通过原型链继承。
继承方式 |
示例 |
原型链继承 |
Sub.prototype = new Super(); |
构造函数继承 |
function Sub(){ Super.call(this);} |
组合继承 |
|
原型式继承 |
Object.create(prototype) |
Chapter five . namespace
以上我们接触了全局变量和this的概念,前端JS开发中,不规范的命名规则会导致JS全局变量混乱甚至冲突。
下面这段代码示例规范变量命名空间
var
GLOBAL = {};
GLOBAL.namespace
= function(str){
var arr = str.split(‘.‘);
var start = 0;
if(arr[0] == ‘GLOBAL‘){ start = 1; }
for(var i = start; i < arr.length; i++){
GLOBAL[arr[i]] = GLOBAL[arr[i]] || {};
GLOBAL = o[arr[i]];
}
};
假设a变量完成A功能中A1子功能。b变量完成A功能中A2子功能。c变量完成B功能。那么
GLOBAL.namespace(‘A.A1);
A.A1.a = …;
GLOBAL.namespace(‘A.A2);
A.A2.b = …;
GLOBAL.namespace(‘A.A1);
B.c = …;
同时,
尽量在匿名函数中声明变量而非全局环境中。
为代码添加更多注释。
Chapter six . reference
火狐开发者JS文档
https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript
IBM开发者社区
http://www.ibm.com/developerworks/cn/web/
有关ECMA-262个人站点