作用域链每段代码(全局代码或函数)都有一个与之关联的作用域链(scope chain),这个作用域链是一个对象列表或者链表,这组对象定义了这段代码‘作用域中’的变量:最顶层代码:作用域链由一个全局对象组成不包含嵌套的函数体内:作用域链上有两个对象 第一个是定义函数参数和局部变量的对象 第二个是全局对象在一个嵌套函数体内:作用域链上至少有三个对象对象链的创建规则:定义一个函数时,保存一个作用域链;调用此函数时,创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的作用域链上,同时创建一个新的更长的表示函数调用作用域的链;对于嵌套函数:调用外部函数时,内部函数都会重新定义一遍(因为每次调用外部函数时,作用域链都是不同的),每次调用外部函数时,内部函数的代码都是相同的,但是关联这段代码的作用域链不同。表达式原始表达式表达式的最小单位,其不再包含其他表达式,包含常量或直接量、关键字和变量;对象和数组的初始化表达式函数定义表达式 函数直接量属性访问表达式得到一个对象属性或者数组元素的值调用表达式先计算函数表达式,然后计算参数表达式如果函数表达式是属性访问表达式,则称为方法调用,在方法内,this指向该对象或者数组;对象创建表达式无参数可以省略括号。方法内this指代新创建的对象;运算符算术表达式、比较表达式、逻辑表达式、赋值表达式等使用运算符;左值:表达式只能出现在赋值运算符的做的,变量,对象属性和数组元素均是左值;内置函数可以返回左值,但是自定义函数不能返回左值。运算符的副作用:+ ++ -- delete会产生副作用默认操作数类型转换:+ - * / % ==delete 删除对象属性或者数组元素typeof 检测操作数类型 操作数可以是任意类型所有对象和数组以及null运算结果是'object',可用来区分对象和原始类型所有函数都是可执行对象,但是不是所有的可执行对象都是函数;所有可执行对象进行typeof运算都返回function如果需要区分对象的类,需要使用instanceof,class特性,constructor属性void 返回undefinedinstanceof 测试对象是否是类的实例 左操作数是一个对象,右操作数标识对象的类;此判断通过原型链检测,包含父类的检测。原型链是javascript的继承机制;instanceof运行过程:为了计算 o instanceof f,首先计算f.prototype,然后在原型链中查找o.对象o中存在一个隐藏的成员,这个成员指向其父类的原型,如果父类的原型是另外一个类的实例的话,则这个原型对象中也存在一个隐藏成员指向另外一个类的原型,这种链条将许多对象或者类串接起来,就是原型链。in 测试属性是否存在 左操作数是一个字符串或者可以转换为字符串的值,右操作数是一个对象, 忽略第一个操作数,返回第二个操作数表达式计算eval(); 使用调用它的变量作用域环境;
var在函数体内,使用var定义变量,其作用域为此函数;如果在顶层代码中使用var,则声明的是全局变量;var声明的全局变量不能删除;function函数声明语句与函数定义表达式:函数声明语句与变量声明一样,会被提前;但是函数定义表达式中的赋值不会被提前;function声明的函数无法删除,但可重写。for in遍历对象属性成员,只能枚举可枚举属性;如果第一个操作数是对象,则会将枚举所得值赋给这个对象。标签标志符:语句; //定义标签continue 标志符 //使用break或者continuewith用于临时扩展作用域链,语法:with(object)statement上述语句将object添加到作用域链的头部,然后臧星statement,最后把作用域链回复到初始状态;debugger用来产生一个断点;use strict说明后续代码将会解析为严格代码;对象创建对象对象直接量关键字newObject.create(objectPrototype)原型:每一个javascript对象(null除外)都与原型关联,每个对象都从原型继承属性;通过new和构造函数调用创建的对象与通过对象直接量创建的对象都继承自Object.portotype;通过对象直接量创建的对象具有同一个原型对象,可通过Object.prototype获得对原型对象的引用;通过new和构造函数调用创建的对象的原型就是构造函数的prototype属性的值。Object.prototype没有原型,它不继承任何属性;其他原型对象都是普通对象,普通对象都有原型。属性的查询和设置object.propertyNameobject[propertyName]作为关联数组的对象使用.访问对象的属性,属性名用一个标志符来表示,无法在运行过程中动态指定标志符;使用[]访问对象属性,属性名通过字符串来表示,可以在运行过程中动态指定。继承父类属性可以被子类中同名的属性覆盖;属性赋值会首先检查原型链,判断父类中此属性是否允许属性赋值操作,如果是只读属性,则赋值操作不被允许;如果允许赋值操作,则在子类对象上创建属性或者对已有属性赋值,而不会修改原型链。只有在查询属性才会追溯原型链,而设置属性则与继承无关,此特性可以让子类有选择性地覆盖继承自父类的属性。所以:属性赋值要么失败,要么在子类对象上创建、修改属性。例外:如果父类对某一属性提供了一个setter方法的accessor属性,则在设置属性时,将会调用setter方法,而不是给子类创建一个属性;但是此操作只针对子类对象本身,并不会修改原型链。属性访问错误var len = book && book.title && book.title.length;删除属性delete只是断开属性和宿主对象的联系,不会操作属性中的属性;且只能删除自有属性,而不能删除继承属性;不能删除可配置性为false的属性;var,function声明的全局变量和全局函数不能删除。检测属性in运算符'propertyName' in Object 左侧是属性名(字符串),右侧是对象,如果对象的自有或者继承属性中包含此属性则返回true;Object.hasOwnPreperty(propertyName) 检测给定的名字是否是对象的自有属性Object.propertyIsEnumerable(propertyName) 检测是否是自有可枚举属性枚举属性for/in 遍历对象的可配置属性,将属性名赋给循环变量;对象继承的内置方法不可枚举,无特殊设置的代码中给对象添加的属性都是可枚举的。Object.keys() 发囊是对象中可枚举自由属性的名称组合Object.getOwnPropertyNames() 返回对象的所有自有属性的名称;属性getter和setter普通数据属性存取器属性定义不同与普通数据属性,也不同于方法:给对象直接量定义存取器属性:get propertyName(){},set propertyName(newValue){}存取器定义中,this指代当前对象(同对象的方法);存取器属性与普通数据属性一样,可继承;存取器的使用方式与普通属性访问方式一致,使用.或者[]即可。属性的特性一个属性包含一个属性名和四个特性:值、可写性、可枚举性和可配置性;(value,writable,enumerable,configurable)存取器不具有值特性和可写性,其可写性是有setter方法存在与否决定的;所以存取器的特性有:读取,写入,可配置性和可枚举性;(get,set,enumerable,configurable)通过调用Object.getOwnPropertyDescriptor()可以获得某个对象特定自有属性的属性描述符;通过调用Object.definePeroperty()可以创建或者修改自有属性的特性,也可以为自有属性增加存取器属性;Object.defineProperty(o,'x',{get:function(){return 0;}});如果需要同时创建或者修改多个属性的特性,可以使用Object.defineProperties();对象的三个属性每个对象都有与之相关的原型(prototype),类(class)和可扩展性(extensible attribute).原型属性Object.getPrototypeOf()可以查询对象原型;通过new表达式创建的对象,通常继承一个constructor属性,此桑墟ing指代创建这个对象的构造函数;通过直接量或者Object.create()创建的对象包含一个constructor属性,此属性指代Object()函数;所以constructor.prototype才是对象直接量的真正的原型,但Object.create()创建的对象不是这样。可以使用p.isPrototypeOf(o)来检测p是否是o的原型;类属性一个字符串,用于表示对象的类型信息。只能间接获取。通过内置构造函数创建的对象包含类属性,它与构造函数名称相匹配;通过自定义,对象直接量和Object.create创建的对象的类属性是'Object',所以通过类属性无法区分对象的类。function classof(o) { if(o === null) return 'NULL'; if(o === undefined) return 'Undefined'; return Object.prototype.toString.call(o).slice(8,-1); }
可扩展性对象的可扩展性用于表示是否可以给对象添加新属性;通过将对象传入Object.isExtensible()判断对象是否可扩展;通过调用Object.preventExtensions()将对象转换为不可扩展,此操作是不可逆的,且仅影响当前对象本身,对继承属性无影响,如果对父类添加属性,此类仍然可以继承到。Object.seal();Object.isSealed();Object.freeze();Object.isFrozen();
序列化对象
JSON.stringify()
JSON.parse()
对象方法
toString() 将对象转换为字符串时调用
toLocaleString()
toJSON()valueOf() 将对象转换为非字符串的原始值时调用
函数
函数调用
作为函数
作为方法 函数表达式是一个属性访问表达式——该函数是一个对象的属性或数组元素,则此调用为方法调用;this没有作用域限制,嵌套函数不能从调用它的函数中继承this;如果在内部函数中需要访问外部函数的调用上下文,则需要将外部函数的调用上下文保存在变量中,提供给内部函数使用。
作为构造函数 新创建的对象继承自构造函数的prototype属性;
通过call()和apply()
函数实参与形参
传入实参少于形参个数
js对函数调用参数类型和参数个数不会检查,所以需要考虑对可选参数没有传入的情况进行兼容;当调用函数时传入的实参比函数声明时的指定形参个数少,剩余形参都将设置为undefined;
传入实参多于形参个数
参数对象——arguments指向实参的引用,它是一个类数组对象,可以通过数字下标访问传入的实参值。
不定实参函数,可以接受任意多个实参;可以使用arguments[i] = newValue;来修改实参值;
实参对象其他属性:callee 指代当前正在执行的函数
caller 指代当前正在执行的函数的函数;通过caller可以访问调用栈;
作为值的函数javascript中,函数不仅可以定义和调用,还可以作为值,可以将函数赋给变量,存储在对象的属性或数组的元素中,作为一个参数传入另一个函数等。function add(opreator1,operator2){ return operator1+operator2;} function opreate(operation,opreator1,opreator2) { return opreation(operator1,operator2); }
Array.sort()中传入的排序规则就是将一个函数作为参数;作为对象的函数——函数自定义属性函数作为一个对象,也可以拥有属性;当函数需要一个‘静态’变量来在调用时保持某个值不变,就可以给函数定义一个自定变量。uniqueInteger.counter = 0; function uniqueInteger(){ return uniqueInteger.counter++; } function factorial(n) { if(isFinite(n) && n>0 && n == Math.round(n)) { if(!(n in factorial)) { factorial[n] = n*factorial(n-1); } else return factorial[n]; } else return NaN; }
作为命名空间的函数利用javascript变量作用域的特性,可以将函数作为命名空间。function mymodule() { //模块代码 //这个模块所使用的所有变量都是局部变量 //这里的变量不会污染到全局命名空间 } mymodule(); //需要调用此方法变量才会被创建 /** * 定义匿名函数并在单个表达式中立即调用 */ (function() { }()); 例子: var extend = (function(){ for(var p in {toString:null}) { return function extend(o) { for(var i = 1;i<arguments.length;i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; } return o; }; } return function patched_extend(o){ for(var i =1;i<arguments.length;i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; for(var j=0;j<protoprops.length;j++) { prop = protoprops[j]; if(source.hasOwnProperty(prop)) o[prop] = source[prop]; } } return o; }; //因为提前问题,会不会出现protoprops的值在函数中使用时为undefined的问题 var protoprops = ['toString','valueOf','constructor','hasOwnProperty','isPrototypeOf' ,'propertyIsEnumerable','toLocaleString']; }());
上述例子中先使用变量,然后才对变量进行了定义(重点是初始化,因为变量定义会被提前,但是初始化不会被提前),会不会出现问题,我做了如下测试:结果显示确实有问题。![]()
function look() { for(var j=0;j<protoprops.length;j++) { console.log(protoprops[j]); } //因为提前问题,会不会出现protoprops的值在函数中使用时为undefined的问题 var protoprops = ['toString','valueOf','constructor','hasOwnProperty','isPrototypeOf' ,'propertyIsEnumerable','toLocaleString']; } look();
闭包函数的执行依赖于变量作用域;函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中成功为闭包;当一个函数嵌套了另外一个函数,外部函数将嵌套函数的函数对象作为返回值返回时,调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链。var scope = 'global scope'; function checkscope() { var scope = 'local scope'; function f() { return scope; } return f; } console.log(checkscope()());
javascript函数的执行用到了作用域链,作用域链时函数定义时创建的。在一个嵌套函数体内,作用域链上至少有三个对象,第一个是定义嵌套函数的参数和局部变量的对象,第二个是定义外部函数参数和局部变量的对象,第三个是全局对象。
当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来储存它的局部变量,并把这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的‘链’。
嵌套函数在调用时将自己的局部变量(可以访问当外部函数的局部变量和参数)捕捉到并保存在作用域链中,看起来像这些变量绑定到了外部函数。
var uniqueInteger = (function(){ //定义函数并立即调用 var counter = 0; //counter作为匿名函数的私有属性,只有嵌套函数可以进行 //操作,这样就可以避免恶意修改 return function(){return counter++;}; }()); /** * 多个嵌套函数,多个闭包共享一个外部函数私有变量 * @returns {{count: (function(): number), reset: reset}} */ function counter(){ var n =0; return{ count:function(){return n++;}, reset:function(){n=0;} }; } /** * 将上述闭包合并为属性存取器方法 * 且没有定义局部变量,使用外部函数参数作为局部变量保存counter * @param n * @returns {*} */ function counter(n){ return { get count(){ return n++;}, set count(m){ if(m >= n) n=m; else throw new Error('参数小于当前值!'); } }; } /** * 利用闭包实现的私有属性存取器方法 * getter,setter,value保存在方法的局部变量中,是私有的,没有办法绕过存取器方法设置或修改value; * @param o * @param name * @param predicate */ function addPriveProperty(o,name,predicate) { var value; o['get'+name] = function(){return value;}; o['set'+name] = function(v){ if(predicate && !predicate(v)){ throw new Error(`set ${name}:invalid value ${v}`); } else{ value = v; } }; } var o = {}; addPriveProperty(o,'Name',function(x){ return typeof x === 'string';}); o.setName('Frank'); console.log(o.getName()); o.setName(0); /** * 此方法生成10个闭包,添加到数组中,将数组作为返回值 * 在外部调用此函数并执行返回的闭包时。因为闭包共享了循环标量i,所以每个闭包中i是最有循环执行完毕的值——10 *上面的counter方法,创建多个计数器时,计数器中的闭包单独创建,各自独立,有各自的作用域链,所以互不影响; * 但是此方法中,所有闭包同时创建,共用同一个作用域链,所以造成每个闭包中都输出10的结果。 * @returns {Array} */ function constfuncs(){ var funcs = []; for(var i =0;i<10;i++) funcs[i] = function(){return i;}; return funcs; } //var funcs = constfuncs(); console.log((constfuncs()[5]()));
结论:关联到闭包的作用域链都是活动的,嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照,在闭包执行时局部变量的值就是当前时刻局部变量的值,并不是闭包生成时局部变量的值。闭包中的this、arguments如果闭包在外部函数里是无法访问this的,除非外部函数将this转存到一个变量让作用域链持有。闭包也无法直接访问外部函数的参数数组——arguments,除非外部函数将arguments将一个变量转存到一个变量让作用域链持有。var self = this; var outerArguments = arguments; 例子: console.log((constfuncs()[5]())); var book = { name:'1', age:2, test:function() { var self = this; var outerArguments = arguments; return { getOuterArgumentsCount:function(){ return outerArguments.length; }, getOuterThisName:function(){ return self.name; } }; } }; console.log(book.test(1,2,3,4).getOuterArgumentsCount()); console.log(book.test().getOuterThisName());
函数属性、方法和构造函数length属性只读属性,代表函数形参的数量。/** * caller 返回当前方法的调用者引用,可用来访问调用栈 * callee 返回正在执行的函数的引用,它是arguments的属性 * @param args */ function check(args) { var actual = args.length; var expected = args.callee.length;//获取当前正在执行的函数定义的形参个数(期望获得的实参个数) if(actual != expected) { throw Error('传入参数个数不正确!'); } } function f(x,y,z) { check(arguments); return x+y+z; } f(1,2,3,4);
prototype属性每一个函数都包含一个prototype属性,这个属性指向一个对象的引用,此对象称为“原型对象”;当将函数用作构造函数时,新创建的对象会从原型对象上继承属性。call/applycall()和apply()的第一个实参是要调用的函数的母对象——函数调用上下文,在函数体内,this指向它;call方法第一个参数之后的其他参数,都是执行方法时的实参;apply方法的第一个实参之后的参数是一个包含方法执行时的所有实参的数组;可以直接传入当前函数的arguments作为参数。function House(name,age,belonger) { this.name = name; this.age = age; this.belonger = belonger; House.prototype.toString = function (){ console.log(`House overrite toString function: name:${this.name},age:${this.age},belonger:${this.belonger}`); } } var house = new House('dfikd',12,'dkfjd'); house.toString(); console.log(Object.prototype.toString.call(house)); /** * 类似于java中的增强效果(反射调用),可以在执行方法前后执行特定的方法,实现切面等等 * @param o * @param m */ function trace(o,m) { var original = o[m]; //获取保存在参数中的原始函数 o[m] = function() { console.log(new Date(),'Entering:',m); var result = original.apply(this,arguments); //调用原始函数 这里this指代 console.log(new Date(),'Exiting:',m); return result; }; } var o = { name:'x', age:'y', sayHello:function() { return `hello,i am ${this.name}`; } }; trace(o,'sayHello'); console.log(o.sayHello());
bind()方法bind方法将函数绑定到指定对象上,并返回对象的方法的引用,可以实现将函数转换为指定对象的方法调用。function f(y){return this.x + y;} var o = {x:1}; var g = f.bind(o); console.log(g(2)); //自己实现bind: function bind(f,o){ if(f.bind) return f.bind(o); else return function(){ return f.apply(o,arguments); } }
除了可以将函数绑定至对象外,传入bind的实参会绑定到this.可以应用于柯里化——函数式编程;传入的第一个参数绑定到this,后续参数依次绑定函数的形参,在调用方法时的参数也会依次绑定方法的形参;这样就可以实现函数有多个形参,但是在调用分两次(绑定和方法调用)传入;如果在方法被调用执行时,函数参数缺少,可能会引起错误。/** * 函数柯里化1 */ var sum = function(y,z){ return this.x+y+z; }; var succ = sum.bind({x:1},1,3); console.log(succ(2,5)); //=>5 /** * 函数柯里化2 */ var sum = function(y,z){ return this.x+y+z; }; var succ = sum.bind({x:1},1); console.log(succ(2,5)); //=>4 /** * 函数柯里化3 */ var sum = function(y,z){ return this.x+y+z; }; var succ = sum.bind({x:1},1); console.log(succ()); //=>NaN /** * 标准版的bind实现 */ if(!Function.prototype.bind) { //必须传入参数 o,其他参数可选 Function.prototype.bind = function(o){ var self = this;//调用bind方法的对象——需要绑定的方法 var boundArgs = arguments;//绑定时传入的参数 return function(){ var args = [],i; for(i=1;i<boundArgs.length;i++) args.push(boundArgs[i]);//将在绑定函数时的参数(从第二个参数开始)放入args中 for(i=0;i<arguments.length;i++) args.push(arguments[i]);//将完成绑定后,执行此方法时的参数也放入args中 return self.apply(o,args); //返回完成绑定的方法 } } }
Function()构造函数var f = new Function('x','y','return x*y;');//等价于var f = function(x,y){return x*y;};
注意点:1.此种方式,如果函数无形参,则可以省略形参,直接传入函数体即可;2.此种方式允许在运行时动态创建并编译函数;3.每次调用,都会重新创建新的函数对象;4.此种方式创建的函数并不使用此法作用域,函数体代码的编译总是会在顶层函数执行,所以无法捕获局部作用域,只能使用全局作用域中的全局变量。可调用对象所有的函数都是可调用对象,但不是所有的可调用对象都是函数;常见的可调用对象是RegExp对象,可以直接调用RegExp对象;使用如下方法可以检测一个对象是否是真的函数对象:function isFunction(x){ return Object.prototype.toString().call(x) === '[object Function]'; }
函数式编程使用函数处理数组高阶函数不完全函数记忆类和模块类和原型类的所有实例对象都从同一个原型对象上继承属性,所以原型对象是类的核心;/** * 通过原型继承创建一个对象 * 通过此方法,可以防止库函数被无意间修改 * @param p * @returns {*} */ function inherit(p) { if(p == null) throw TypeError(); if(Object.create) return Object.create(p); var t = typeof p; if(t != 'object' && t != 'function') throw TypeError(); function f(){}; f.prototype = p; return new f(); }
例:使用工厂方法定义类function range(from,to) { var r = inherit(range.methods); //函数内变量可以与函数同名,r继承于range.methods r.from = from; r.to = to; return r; } range.methods = { includes:function(x){ return this.from<=x && x<=this.to; }, foreach:function(f){ for(var x = Math.ceil(this.from);x<=this.to;x++) f(x); }, toString:function(){return `(${this.from}...${this.to}`;} }; var r = range(-1,3); console.log(r.includes(2)); r.foreach(console.log); console.log(r);
结论:prototype中的属性,是非私有属性,对所有实例是共享的,实例自定义的属性由实例私有。类和构造函数调用构造函数创建新对象,构造函数的prototype属性被用作新对象的原型;所以同过同一个构造函数创建的所有对象都继承于同一个对象。/** * 使用构造函数创建对象 * @param from * @param to * @constructor */ function Range(from,to) { this.from = from; this.to = to; } Range.prototype = { constructor:Range,//如果没有设置此属性,则constructor属性为[Function: Object],并不 //指向Range includes:function(x){ return this.from<=x && x<=this.to; }, foreach:function(f){ for(var x = Math.ceil(this.from);x<=this.to;x++) f(x); }, toString:function(){return `(${this.from}...${this.to}`;} }; var r = new Range(-1,3); console.log(r.includes(2)); r.foreach(console.log); console.log(r);
构造函数和类的标识instanceof原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,它们才时属于同一个类的实例;而构造函数则不能作为类的标识,两个不同的构造函数的prototype有可能指向同一个对象。当使用instanceof运算符来检测对象是否属于某个类时,会使用到构造函数,因为构造函数名称即为类名,但是不会检测对象的构造函数是否是Range()。只有当一个对象继承自Range.prototype, r instanceof Range 才会返回true;constructor属性任何函数都可以做构造函数,每个函数(除Function.bind()获得的方法外)都自动拥有一个prototype属性,此属性指向原型对象,原型对象包含唯一一个不可枚举属性constructor,指向构造函数;var F = function(){}; //构造函数 var p = F.prototype; //构造函数的原型 var c = p.constructor; //构造函数的原型的constructor属性 console.log(c === F); //构造函数的原型的constructor属性指向构造函数
那么,如果多个构造函数的prototype指向同一个对象,此原型对象的constructor的值是什么?var F = function(){}; //构造函数 var G = function(x){this.x = x;}; var o = { includes:function(x){ return this.from<=x && x<=this.to; }, foreach:function(f){ for(var x = Math.ceil(this.from);x<=this.to;x++) f(x); }, toString:function(){return `(${this.from}...${this.to}`;} }; console.log("----------------------------------------"); F.prototype = o; G.prototype = o; var f = F.prototype; //构造函数的原型 var cf = p.constructor; //构造函数的原型的constructor属性 console.log(cf === F); //构造函数的原型的constructor属性指向构造函数 var g = G.prototype; var cg = g.constructor; console.log(cg === G); console.log(g === f); console.log(cf === cg); console.log(cf); console.log(g);
构造函数与原型对象的关系
//var F = function(){}; //构造函数 function F(){} F.prototype = {}; //设置对象原型为某个对象后,对象原型的constructor 属性指向Object() var p = F.prototype; //构造函数的原型 var c = p.constructor; //构造函数的原型的constructor属性 console.log(c === F); // =>false 构造函数的原型的constructor属性 console.log(c); //=>[Function: Object] 修改的方法有两种:为原型对象加入constructor属性或者不重新原型对象,而是扩展原有
原型对象;//var F = function(){}; //构造函数 function F(){} F.prototype = {constructor:F,name:'F'}; //设置对象原型为某个对象后,对象原型的constructor 属性指向Object() var p = F.prototype; //构造函数的原型 var c = p.constructor; //构造函数的原型的constructor属性 console.log(c === F); // =>false 构造函数的原型的constructor属性 console.log(c); //=>[Function: F]
或者://var F = function(){}; //构造函数 function F(){} //F.prototype = {constructor:F,name:'F'}; //设置对象原型为某个对象后,对象原型的constructor 属性指向Object() F.prototype.name = 'F'; var p = F.prototype; //构造函数的原型 var c = p.constructor; //构造函数的原型的constructor属性 console.log(c === F); // =>false 构造函数的原型的constructor属性 console.log(c); //=>[Function: F]
javascript定义类的步骤:第一步,定义构造函数,设置私有属性和方法并进行私有属性初始化;第二步,给构造函数的prototype对象定义实例共享的属性和方法;第三步,给构造函数定义类字段和类属性var extend_1 = (function(){ for(var p in {toString:null}) { return function extend(o) { for(var i = 1;i<arguments.length;i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; } return o; }; } //因为提前问题,会不会出现protoprops的值在函数中使用时为undefined的问题 var protoprops = ['toString','valueOf','constructor','hasOwnProperty','isPrototypeOf' ,'propertyIsEnumerable','toLocaleString']; return function patched_extend(o){ for(var i =1;i<arguments.length;i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; for(var j=0;j<protoprops.length;j++) { prop = protoprops[j]; if(source.hasOwnProperty(prop)) o[prop] = source[prop]; } } return o; }; }()); /** * 定义简单类的函数 * @param constructor 构造函数 其中定义了此类扩展的属性和方法 * @param methods 类实例共享的方法 这些方法将会被复制到构造方法的prototype中去 * @param statics 类的实例属性 这些方法将会被复制到构造方法中去 */ function defineClass(constructor,methods,statics) { if(methods) extend_1(constructor.prototype,methods); if(statics) extend_1(constructor,statics); return constructor; } function Range_1(from,to) { this.from = from; this.to = to; } var rangeMethods = { includes:function(x){ return this.from<=x && x<=this.to; }, foreach:function(f){ for(var x = Math.ceil(this.from);x<=this.to;x++) f(x); }, toString:function(){return `(${this.from}...${this.to}`;} }; var rangeStatics = { upto:function(t){ return new SimpleRange(0,t); } }; var SimpleRange = defineClass(Range_1,rangeMethods,rangeStatics); console.log("+++++++++++++++++++++++++++++++++++++++++++++"); var range_defined_instance = SimpleRange.upto(6); console.log(range_defined_instance.includes(2)); console.log(range_defined_instance.from,range_defined_instance.to); //console.log(range_defined_instance.upto(3)); 所谓类方法,就是构造函数的方法,实例不可能拥有 console.log(`${range_defined_instance.from},${range_defined_instance.to}`);
另外一种定义类的方式:手动实现类的构造函数,实例字段,实例方法,类字段,类方法;类的扩充使用Object.prototype.newAttribute = value;可以对类进行扩展。类和类型三种检测任意对象的类的技术:instanceof constructor 构造函数的名称instanceof左操作数是待检测的对象,右操作数是类的构造函数;如果对象o继承自c.prototype,则 o instanceof c 值为true;这里的继承可以不是直接继承。如果不想使用构造函数,则可以使用 prototypeObject.isPrototypeOf(r);缺点:无法通过对象获得类名,只能检测对象是否属于指定的类名。constructor对象的constructor属性继承自原型对象的constructor,指向构造函数;function typeAndValue(x) { if(x === null) return ''; switch (x.constructor) { case Number: return `Number:${x}`; case String: return `String:${x}`; case Date: return `Date:${x}`; case RegExp: return `RegExp:${x}`; } }
缺点同instanceof;构造函数的名称function type(o) { var t,c,n;//type,class,name if(o === null) return 'null'; if(o !== o) return 'NaN'; if((t = typeof o) !== 'object') return t; if((c = classof(o)) !== 'Object') return c; if(o.constructor && typeof o.constructor === 'function' && (n = o.constructor.getName())) return n; return 'Object'; } function classof(o){ //对象使用Object定义的toString时,会生成类似于如下的类信息描述:[object Object] //为了避免部分类对toString的覆写,需要将该Object的toString方法绑定到该对象上执行,然后再从结果中截取第八个字符到倒数第二个字符,就可以获得类名 return Object.prototype.toString.call(o).slice(8,-1); } Function.prototype.getName = function(){ if('name' in this) return this.name; return this.name = this.toString().match(/function\s*([^(]*)\(/)[1]; };
问题:并不是所有的方法都有名字,也不是所有的对象都具有constructor属性;
鸭式辩型
关注对象可以做什么,只要对象满足一定的条件,就认为它可以使用。类似于java中的多态的思想。
javascript中的面向对象技术
集合类实现
枚举类型
标准转换方法
toString() 对象转换为字符串
toLocaleString() 对象转换为本地化字符串
valueOf() 将对象转换为原始值
toJSON() 执行对象序列化 JSON.stringify()自动调用,忽略对象的原型和构造函数。
比较方法
是否相等 equals
javascript的相等运算比较对象时,比较的时引用而不是值;需要自定义equals方法按照我们需要的方式对对象是否相等进行判断。
比较大小 compareToRange.prototype.equals = function(that){ if(!that) return false; if(that.constructor !== Range) return false; return this.from === that.from && this.to === that.to; }; Set.prototype.equals = function (that) { if(this === that) return true; if(!(that instanceof Set)) return false; if(this.size !== that.size) return false; try{ this.foreach((v)=>{ if(!that.contains(v)) throw false; }) return ture; }catch(x){ if(x === false) return false; throw x; } };
如果使用>或者<对对象比较大小,则首先会调用valueOf(),有些对象没有实现valueOf(),为了能够显式比较对象大小,需要定义compareTo()方法指定对象比较规则,按照我们的标准对对象大小进行比较。
约定:this>that 返回值小于0
this=that 返回值等于0
this<that 返回值小于0
equals和conpareTo方法关于相等的判断规则最好能够保持一致:Range.prototype.compareTo = function(that) { return this.from - that.from; }
需要定义一个能够比较两个参数的compareTo()方法用于Array.sort()使用。Range.prototype.compareTo = function(that) { if(!(that instanceof Range)) throw new Error('不是同一个类的实例,不能比较'); var diff = this.from - that.from; if(diff === 0) diff = this.to - that.to; return diff; }
方法借用Range.byLowerBound = function(a,b){return a.compare(b);}; ranges.sort(Range.byLowerBound);
多个类中的方法可以共用一个单独的函数;
还可以定义自定义泛型方法:Set.prototype.toJSON = Set.prototype.toArray;
(将整个Set的实现放在这里,其中包含了自定义泛型方法。)
私有状态/** * 构造函数 * @constructor */ function Set(){ //初始化属性值 this.values = {}; this.n = 0; this.add.apply(this,arguments); //调用add方法将参数加入到values中; } Set.prototype.add = function(){ for(var i =0;i<arguments.length;i++){ var val = arguments[i]; var str = Set._v2s(val); if(!this.values.hasOwnProperty(str)){ this.values[str] = val; this.n++; } } return this; }; Set.prototype.remove = function(){ for(var i = 0;i<arguments.length;i++){ var str = Set._v2s(arguments[i]); if(this.values.hasOwnProperty(str)){ delete this.values[str]; this.n--; } } return this;//支持链式方法调用 }; Set.prototype.contains = function(value){ return this.values.hasOwnProperty(Set._v2s(value)); }; Set.prototype.size = function(){ return this.n; }; Set.prototype.foreach = function (f,context) { for(var s in this.values){ if(this.values.hasOwnProperty(s)) f.call(context,this.values[s]); } }; Set.prototype.equals = function (that) { if(this === that) return true; if(!(that instanceof Set)) return false; if(this.size !== that.size) return false; try{ this.foreach((v)=>{ if(!that.contains(v)) throw false; }) return ture; }catch(x){ if(x === false) return false; throw x; } }; Set.prototype.toString = function(){ var s = '{',i = 0; this.foreach((v)=>{ s += ((i++>0)?', ':'') +v; }); return s + '}'; }; Set.prototype.toLocaleString = function(){ var s = '{',i=0; this.foreach((v)=>{ if(i++>0) s += ', '; if(v === null) s += v; else s += v.toLocaleString(); }); return `${s}}`; }; Set.prototype.toArray = function(){ var a = []; this.foreach(function (v){a.push(v);}); return a; }; Set.prototype.toJSON = Set.prototype.toArray; Set._v2s = function (val){ switch (val) { case undefined : return 'u'; case null : return 'n'; case true : return 't'; case false : return 'f'; default : switch(typeof val){ case 'number':return '#'+val; case 'string':return '\'\''+val; default:return '@'+objectId(val); } } function objectId(o){ var prop = '|**objectid**|'; if(!o.hasOwnProperty(prop)) o[prop] = Set._v2s.next++; return o[prop]; } }; Set._v2s.next = 100; console.log('-------------------SET---------TEST--------------------------------'); var a = new Set(1,2,3,4,5,6,'a','b','c'); a.add('d'); a.foreach(console.log); console.log(a.contains('a')); console.log(a.size()); a.remove(1); a.foreach(console.log); console.log(a.contains(1)); console.log(a.size()); console.log(a.toString()); console.log(a.toLocaleString()); console.log(a.toJSON()); console.log('-------------------SET---------TEST--------------------------------'); //方法借用的泛型实现 可以给多个类共享使用此方法 var generic = { toString:function(){ var s = '['; if(this.constructor && this.constructor.name) s += this.constructor.name; var n = 0; for(var name in this){ if(!this.hasOwnProperty(name)) continue; var value = this[name]; if(typeof value === 'function') continue; if(n++) s += ','; s += `${name} = ${value}`; } return s + ']'; }, euqals:function(that){ if(!that === null) return false; if(this.constructor != that.constructor) return false; for(var name in this) { if(name === '|**objectid**|') continue; if(!this.hasOwnProperty(name)) continue; if(this[name] !== that[name]) return false; } return false; } };
可以通过将变量或者参数闭包在一个构造函数内来模拟实现私有实例字段;但是使用闭包实现代码运行内存开销会增加。
构造函数的重载和工厂方法function Range (from,to){ this.from = function () { return from; } this.to = function () { return to; } } Range.prototype = { constructor:Range, includes:function (x) { return this.from() <=x && this.to()>=x; }, foreach:function (f) { for(var x = Math.ceil(this.from()),max = this.to();x<max;x++) f(x); }, toString:function () { return `${this.from()}...${this.to()}`; } };
也可以定义多个构造函数,将构造函数的prototype指向同一个对象,可以实现构造函数的重载;但是不推荐。/** * 构造函数 * @constructor */ function Set(){ //初始化属性值 this.values = {}; this.n = 0; if(arguments.length === 1 && Array.isArray(arguments[0])) this.add.apply(this.arguments[0]); else if(arguments.length>0) this.add.apply(this,arguments); //调用add方法将参数加入到values中; } /** * 工厂方法 通过数组创建集合 * @param a * @returns {Set} */ Set.fromArray = function(a){ s = new Set(); s.add.apply(s,a); return s; };
子类
在继承实现的过程中,首先要确保子类的原型对象继承自父类的原型对象:
以下实现B继承A:
构造函数和方法链function inherit(p){ if(p === null) throw '参数不能为Null!'; if(Object.create) return Object.create(p); var t = typeof p; if(t !== 'object' && t !== 'function') throw TypeError(); function f(){}; f.prototype = p; return new f(); } //B.prototype = inherit(A.prototype); //B.prototype.constructor = B; var extend = (function(){ for(var p in {toString:null}) { return function extend(o) { for(var i = 1;i<arguments.length;i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; } return o; }; } //因为提前问题,会不会出现protoprops的值在函数中使用时为undefined的问题 var protoprops = ['toString','valueOf','constructor','hasOwnProperty','isPrototypeOf' ,'propertyIsEnumerable','toLocaleString']; return function patched_extend(o){ for(var i =1;i<arguments.length;i++) { var source = arguments[i]; for(var prop in source) o[prop] = source[prop]; for(var j=0;j<protoprops.length;j++) { prop = protoprops[j]; if(source.hasOwnProperty(prop)) o[prop] = source[prop]; } } return o; }; }()); function defineSubClass(superclass,constructor,methods,statics) { constructor.prototype = inherit(superclass.prototype); constructor.prototype.constructor = constructor; if(methods) extend(constructor.prototype,methods); if(statics) extend(constructor,statics); return constructor; } Function.prototype.extend = function (constructor,methods,statics) { return defineSubClass(this,constructor,methods,statics); }; function SingletonSet(member){ this.member = member; Set.apply(this,arguments); } SingletonSet.prototype = inherit(Set.prototype); extend(SingletonSet.prototype,{ constructor:SingletonSet, sayHello:function(){console.log('dhfa;dsfa;df');} });
使用类工厂技术扩展过滤集合:
组合vs子类var StringSet = filteredSetSubclass(Set,function(x){return typeof x==='string';}); var MySet = filteredSetSubclass(NonNullSet,function(x){return typeof x !== 'function';}); function filteredSetSubClass(superClass,filter){ var constructor = function () { //子类构造函数 superClass.apply(this,arguments); //调用父类构造函数 构造函数链 }; var proto = constructor.prototype = inherit(superClass.prototype); //子类构造函数的原型继承于父类原型 proto.constructor = constructor; //子类原型的constructor属性指向构造函数 proto.add = function () { for(var i =0;i<arguments.length;i++) { var v = arguments[i]; if(!filter(v)) throw `value ${v} rejected by filter`; //用传入的过滤器进行过滤,符合规则才能加入 } superClass.prototype.add.apply(this,arguments); //调用父类的方法 方法链 } return constructor; }
以上使用子类继承Set实现不同类型的集合时,每次要将过滤函数和Set组合,都需要创建一个新类;有一种替代方案:组合。
设计原则:组合优于继承;组合的好处是只需创建一个单独的FilteredSet子类即可。
类的层次结构和抽象类var FilteredSet = Set.extend(function FilteredSet(set,filter){ this.set = set; this.filter = filter; },{ add:function(){ if(this.filter) { for(var i =0;i<arguments.length;i++) { var v = arguments[i]; if(!this.filter(v)) throw new Error('此值不满足过滤要求!'); } } this.set.add.apply(this.set,arguments); return this; }, remove:function () { this.set.remove.apply(this.set,arguments); return this; }, contains:function(v){return this.set.contains.apply(v)}, size:function () { return this.set.size(); }, foreach:function (f,c) { this.set.foreach(f,c); } }); var s = new FilteredSet(new Set(),function(x){return x !== null;}); var t = new FilteredSet(s,function(x){return x !== null;});
ECMAScript 5中的类function abstractmethod(){throw new Error("Can't excute abstract method");} function AbstractSet(){throw new Error("Can't instantiate abstrct classes");} Abstract.prototype.contains = abstractmethod; //使用Function.prototype.extend进行扩展 /** * 非抽象子类 */ var NotSet = AbstractSet.extend( function NotSet(set){ this.set = set; },{ contains:function (x) { return !this.set.contains(x); }, toString:function (x) { return `~${this.set.toString()}`; }, equals:function (that) { return that instanceof NotSet && this.set.equals(that.set); } } ); /** * 抽象子类 */ var AbstractEnumberableSet = AbstractSet.extend(function () { throw new Error("Can't instantiate abstract class"); },{ size:abstractmethod, foreach:abstractmethod, isEmpt:function(){return this.size === 0;} //还可以定义其他方法 });
让属性不可枚举
使用Object.difineProperty()方法对类的属性进行配置
定义不可变的类
可以使用如下的工具来定义不可变的类
封装对象状态/** * 将o的指定名字的属性设置为不可写和不可配置的 * @param o * @returns {*} */ function freezeProps(o){ var props = (arguments.length ==1) ? Object.getOwnPropertyNames(o) : Array.prototype.splice(arguments,1); props.forEach(function(n){ if(!Object.getOwnPropertyDescriptor(o,n).configurable) return; Object.defineProperty(o,n,{writable:false,configurable:false}); }); return o; } /** * 将o的指定名字的属性设置为不可枚举的 * @param o * @returns {*} */ function hideProps(o){ var props = (arguments.length == 1)?Object.getOwnPropertyNames(o) : Array.prototype.splice(arguments,1); props.forEach(function (n) { if(!Object.getOwnPropertyDescriptor(o,n).configurable) return; Object.defineProperty(o,n,{enumerable:false}); }); return o; }
将构造函数的变量和参数私有化,使用getter和setter对它们进行访问和修改;
防止类的扩展
通过给原型对象添加方法可以动态地对类进行扩展:
可以使用Object.preventExtensions()将对象设置为不可扩展的,就可以限制给对象添加新属性。
可以使用Object.seal()限制给对象添加新属性,还可以将对象的所有属性设置为不可配置(Object.seal(Object.prototype))。
对象的方法可被随时替换:
可以使用上面定义的freezeProps()方法,或者使用Object.freeze()将所有属性设置为只读和不可配置;
如果父类的属性设置为只读,则子类覆写此方法时只能使用Object.defineProperty(),Object.defineProperties()或者Object.create()来创建这个新属性;
子类和ECMAScript5
可以使用Object.create()方法创建子类,但是其所有属性的特性默认都是false;
属性描述符
模块
类不是唯一的模块化代码的方式,模块是一个独立的javascript文件;模块文件可以包含一个类定义、一组相关的类、一个使用函数库或者是一些待执行的代码;只要以模块的形式编写代码,任何javascript代码就可以当做一个模块。
模块化的目标是支持大规模的程序开发,处理分散源中代码的组装,并让代码正确运行;为了能够保证即使出现不正确的模块代码,也能正确执行代码,不同的模块必须避免修改全局执行上下文,后续模块才能在它们所期望的原始上下文中运行;所以,所有模块都不应当定义超过一个全局标识。
用作命名空间的对象
使用对象作为命名空间,将函数和值作为命名空间对象的属性储存起来,而不是定义全局函数和变量;
例如:
如果需要在另一个模块中使用Set,则将Set从sets对象中取出,导入到当前模块中,直接使用,不需要重复引用://将所有集合类的实现都放在一个对象,作为对象的属性保存; var sets ={}; sets.set ={}; sets.SigletonSet = sets.AbstractEnumerableSet.extend(...);
如果模块嵌套较深:var Set = new sets.set; //将模块导入到当前模块中; var s = new Set(1,2,3);//使用时不用重复引用;
通过翻转互联网域名对命名空间命名,这样就可以保证命名空间全局唯一;模块的文件名应当和命名空间匹配;而且文件路径应该与模块名称相同,比如:com.majq.collections.sets.set 模块应该放在com/majq/collections/sets/set.js中。var collections; if(!conllections) collections = {}; collections.sets = {}; collections.sets.AbstractSet = function(){...//类定义};
作为私有命名空间的函数
因为javascript函数内的局部变量在函数外部是不可见的,所以可以将一些不需要模块外可见的函数、类、属性和方法定义在函数内部;这些函数的作用域可以用作模块的私有命名空间(有事称为模块函数),将构造方法作为函数的返回值返回即可。
如果需要放函数在私有命名空间内执行,可以使用:var Set = (function invocation(){ function Set(){ //构造函数实现 } Set.prototype.xxx = function(x){ //实例方法实现 } function v2s(){ //类方法实现(类私有) } return Set; }()); //函数立即执行,得到Set;
就可以实现。(function(){ //这里定义要在私有命名空间内执行的函数 ...... }());
将代码封装到模块中以后,就需要方法导出公用API,以供在模块外部使用它们。可以在封装模块中进行返回:
或者可以将模块函数当做构造函数,通过new来调用,将需要提供出去的API赋值给this,以供外部使用。var collections; collections.sets = (function namespace(){ //定义多种集合类; return{ AbstractSet:AbstractSet, NotSet:NotSet, SingletonSet:SingletonSet, ArraySet:ArraySet, ...//将需要提供给模块外使用的API提供出去 }; }());
或者可以使用如下:var collections; if(!collections) collections = {}; collections.sets = (new function namespace(){ //这里定义很多类型的Set this.ArraySet = ArraySet; this.SingletonSet = SingletonSet; ... //不需要返回值 }());
正则表达式的模式匹配var collections; if(!collections) collections = {}; (new function namespace(){ //这里定义很多类型的Set collections.sets.ArraySet = ArraySet; collections.sets.SingletonSet = SingletonSet; ... //不需要返回值 }());
正则表表达式直接量
var pattern = /s$/;