JavaScript中只有两大类型:原始值和对象;类型通过typeof运算符来得到
原始值的特点是不可修改(在那个特定的内存区域保存的就是这个原始值的值);我们可以让变量指向另一个原始值,但无法改变原始值内部的内容。
undefinednullNumberBooleanStringSymbol: 一种特殊的string序列(独一无二) #####1.2、对象对象本身就是一个引用,它指向一个集合的地址,集合内是对象的属性;我们修改集合内的对象属性,不改变对象的值,只有当我们将对象指向另一个集合时,才会改变对象的值。JavaScript作为一个弱类型的语言,它的对象类型非常灵活。面对如此庞杂的对象,我们要识别它,就引入了类的概念。 ####2、类的概念 #####2.1、类的基本组成 类,顾名思义,就是对对象进行分类和抽象,它主要有下面几部分的内容:实例字段:它们是基于实例的属性或变量,用以保存对立对象的状态;在js中,就是构造函数中this的属性。实例方法:他们是类的所有实例所共享的方法,由每个独立的实例调用;在js中,通过原型来实现,就是constructor.prototype的方法;类字段: 这些属性或变量属于类的,而不是属于类的某个实例的;在js中,就是constructor.properties;类方法: 这些方法属于类的,而不是属于类的某个实例的;在js中,就是constructor.methods;上面的2、3、4项,都是为了实现某种类型的共享。区别主要有:实例方法,可以在函数内部直接使用this来访问实例的属性和方法;而类的方法和字段,类似于全局函数和变量,作为构造函数的属性,只是在标识符上用类名做了一个区分。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M1IQ1eJb-1593672875475)(./asset/proto.jpg)]
以Persion类为例,令 proto = Persion.prototype;类需要具备以下条件:
proto.constructor = Persion;Persion.prototype = proto;instance = Object.create(proto);在构造函数里,我们通过new来实现这个关系,在工厂函数中,我们可以通过手动实现。上面的第三条是类的根本,要检验 instance instanceof Persion 实际上就是 instance的继承链上是否有Persion.prototype,无论是不是直接继承。
/**factory method * 一个简单的范围类 */ function range(from, to) { var r = Object.create(range.prototype);//上面的条件3 r.from = from; r.to = to; return r; } range.prototype = {//上面的条件2 constructor: range, //上面的条件1 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 instanceof range);//true;满足了上面三个条件,所以r是range的实例javaScript中,类主要由constructor、constructor.prototype两部分组成。constructor有两部分:构造函数本身,创建instance数据;constructor的属性是类的静态方法和数据
/**一个定义简单类的函数 */ /**extend * 拓展对象o中的属性(只处理可枚举属性,包括继承来的属性) * 如果o与p中有同名属性,则覆盖o中属性 * @param {Object} o * @param {Object} p * @return {Object} */ function extend(o, p) { for (var prop in p) { o[prop] = p[prop]; //浅复制 } return o; } /**defineClass * 通过构造函数constructor、实例方法method、类数据和类方法static来构造类 * @param {Function} constructor * @param {Object} method * @param {Object} static * @return {Function} */ function defineClass(constructor, method, static) { if (method) extend(constructor.prototype, method); if (static) extend(constructor, static); return constructor; } //constructor function Complex(real, imaginary) { if (isNaN(real) || isNaN(imaginary)) throw TypeError("real and imaginary must be number!"); this.r = real; this.i = imaginary; } //methods var methodForComplex = { add: function(that) { return new Complex(this.r + that.r, this.i + that.i); }, toString: function() { return '{' + this.r + ',' + this.i + '}'; }, equal: function() { return that != null && that.constructor === Complex && this.r === that.r && this.i === that.i; } }; //static var staticForComplex = { ZERO: new Complex(0, 0), ONE: new Complex(1, 0), I: new Complex(0, 1), //static method polar: function(r, angle) { return new Complex(r * Math.cos(angle), r * Math.sin(angle)); } }; Complex = defineClass(Complex, methodForComplex, staticForComplex); var compA = new Complex(2, 3); var compB = Complex.polar(2, Math.PI / 4); console.log(compA, compB, compA.add(Complex.I)); /******************************************************************************* * ES6类的新语法 *****************************************************************************/ class Complex_ES6 { constructor(real, imaginary) { if (isNaN(real) || isNaN(imaginary)) throw TypeError("real and imaginary must be number!"); this.r = real; this.i = imaginary; }; //methods add(that) { return new Complex_ES6(this.r + that.r, this.i + that.i); } toString() { return '{' + this.r + ',' + this.i + '}'; } equal() { return that != null && that.constructor === Complex_ES6 && this.r === that.r && this.i === that.i; } //static methods static polar(r, angle) { return new Complex_ES6(r * Math.cos(angle), r * Math.sin(angle)); } } Complex_ES6.ZERO = new Complex(0, 0); Complex_ES6.ONE = new Complex(1, 0); Complex_ES6.I = new Complex(0, 1); var compA = new Complex_ES6(2, 3); var compB = Complex_ES6.polar(2, Math.PI / 4); console.log(compA, compB, compA.add(Complex_ES6.I));继承是为了重利用之前的定义的类及其方法。
JavaSript类有下面三方面的内容: 实例数据:构造函数内定义的数据,每个实例都有独立的一份;实例共享方法:constructor.prototype的属性,所有实例共享的方法,在函数内部可以通过this访问实例中的数据类数据和方法:所有实例共享的数据和方法,类方法调用方式跟普通函数一样;类数据的调用方法与普通变量一样。只是加了一个类的前缀,方便记忆和区分。 下面定义一个父类,方便后面继承的使用: /**MySet类 */ function MySet() { this.values = {}; this.n = 0; this.add.apply(this, arguments); } MySet._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 '@' + val.objectId; } } }; /**定义不可枚举的属性 */ (function() { /**定义一个不可枚举的属性objectId它被所有对象继承 * 没有定义setter,所以它不可写;同时不可配置,不可删除 */ Object.defineProperty(Object.prototype, 'objectId', { get: idGetter, enumerable: false, constructor: false }); //读取objectId时,直接调用这个函数;它返回idprop属性, //如果没有此属性,分配给它一个独一无二的值。 function idGetter() { if (!(idprop in this)) { Object.defineProperty(this, idprop, { value: nextid++, writable: false, enumerable: false, configurable: false }); } return this[idprop]; } var idprop = "|**objectId**|"; var nextid = 1; }()); MySet.prototype = { constructor: MySet, add: function() { for (var i = 0; i < arguments.length; ++i) { var val = arguments[i]; console.log("MySet._v2s", MySet._v2s); var str = MySet._v2s(val); if (!this.values.hasOwnProperty(str)) { this.values[str] = val; ++this.n; } } return this; }, remove: function() { for (var i = 0; i < arguments.length; ++i) { var val = arguments[i]; var str = MySet._v2s(val); if (this.values.hasOwnProterty(str)) { delete this.values[str]; this.n--; } } return this; }, contains: function(value) { var str = MySet._v2s(value); return this.values.hasOwnProterty(str); }, size: function() { return this.size; }, foreach: function(f, context) { for (var s in this.values) { if (this.values.hasOwnProterty(s)) { f.call(context, this.values[s]); } } } } //添加新的方法到MySet类的原型对象中 function extend(o, p) { for (var prop in p) { o[prop] = p[prop]; //浅复制 } return o; } extend(MySet.prototype, { toString: function() { var s = '{', i = 0; this.foreach(function(v) { s += ((i++ > 0) ? ',' : '') + v; }); return s + '}'; }, //类似与toString,但对于所有的值,都调用toLocalString toLocalString: function() { var s = '{', i = 0; this.foreach(function(v) { if (i++ > 0) s += ','; if (v == null) s += v; else s += v.toLocalString(); }); return s + '}'; }, toArray: function() { var a = []; this.foreach(function(v) { a.push(v); }); return a; } }); MySet.prototype.toJSON = MySet.prototype.toArray; var a = { x: 1 }; var MySet = new MySet(2, 3, '133', 2, a); console.log(MySet);类继承中对上面三个方面的处理:
实例数据(构造函数链):在子类构造函数内部调用父类构造函数(在子类构造函数中添加语句Superclass.apply(this,arguments));实例共享方法(方法链):子类构造函数的原型继承父类构造函数的原型;constructor.prototype=Object.create(Superclass.prototype);继承后,我们可以通过重载父类中的方法。类数据和方法:既然跟普通函数、变量的使用方法一样。那么就可以不用集成,直接通过变量名来使用。下面的例子定义了一个defineSubclass函数,用来定义子类,它实现了方法链的继承。
/** * 一个创建简单子类的函数 * @param {Function} superclass * @param {Function} constructor * @param {Object} methods * @param {Object} statics * @return {Function} */ function defineSubclass(superclass, constructor, methods, statics) { //方法链的继承 constructor.prototype = Object.create(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 defineClass.call(this, constructor, methods, statics); }下面的例子,使用上面定义的方法,定义了一个上节中Set类的子类NonNullSet
var NonNullSet = (function() { var superclass = MySet;//定义父类 return superclass.extend( //constructor(内部有构造函数链的继承) function() { superclass.apply(this, arguments); }, //重构的方法 { add: function() { Array.prototype.forEach.call(arguments, function(val) { if (val == null) throw new Error("Can't add null or undefined to a NonNullSet!"); }); return superclass.prototype.add.apply(this, arguments); } } ); }());工厂模式在javacript中应用广泛,是一个深刻的概念。我们这里只介绍它的一种应用情形:创建多种不同的类,而这些类共享大量相同的属性,我们通过此函数,可以创建功能相似的不同类。
/**类工厂 * 该函数返回一个具体MySet类的子类 * 重写add()方法,对添加的元素做特殊的过滤 * @param {Function} superclass * @param {Function} filter * @return {Function} */ function filteredSetSubclass(superclass, filter) { var constructor = function() { superclass.apply(this, arguments); }; var proto = constructor.prototype = Object.create(superclass); proto.constructor = constructor; proto.add = function() { Array.prototype.forEach.call(arguments, function(val) { if (!filter(val)) throw ("value " + val + " rejected by filter"); }); return superclass.prototype.add.apply(this, arguments); } return constructor; } //使用工厂方法定义子类 var NonNullSet_Factory = filteredSetSubclass(MySet, function(val) { return val != null; });跟子类相比,组合一种完全不同的继承方式:
它的构造函数的参数跟父类不同,没有继承关系它把父类的实例作为自己的一个属性。而这个实例就连接了它跟父类之间的关系它定义与父类相似的方法,在方法内部调用父类的方法,有必要的话,进行适当的改造下面的函数实现了得到的FilteredSet与上面工厂方法的filteredSetSubclass相似,不同的是FilteredSet的第一个参数是一个父类的实例,返回的是子类的实例,而filteredSetSubclass的地一个参数是父类构造函数,返回的是一个子类的构造函数。
/**组合方法 * 实现一个FilterSet,它包装某个指定的“集合”对象, * 并对传入add()方法的值引用某种指定的过滤器。 * 集合类的其他所有核心方法,延续到包装后的实例中。 * 这种方法通过把类对象作为其属性来实现对类方法数据的继承; * 注意:它的构造函数的参数跟其组合的类的结构不同 */ function FilteredSet(set, filter) { this.set = set; this.filter = filter; } FilteredSet.prototype = { constructor: FilteredSet, add: function() { if (this.filter) { Array.prototype.forEach.call(arguments, function(val) { if (!this.filter(val)) throw ("value " + val + " rejected by filter"); }); } this.set.add.apply(this.set, arguments); return this; }, remove: function() { this.set.remove.apply(this.set, arguments); return this; }, contains: function(val) { return this.set.contains(val); }, size: function() { return this.set.size(); }, foreach: function(f, c) { this.set.foreach(f, c); } };下面的例子中,首先定义了两个工具函数来修改属性的特性;然后通过这两个函数实现方法在遍历中的隐藏,通过setter和getter方法实现数据成员的封装。
/**EMACScript5中的类 * 属性特性和对象特性对类的修饰 */ //属性描述工具函数 //将o指定名字的属性(或所有属性)设置为不可写的和不可配置的 function freezeProps(o) { var props = (arguments.length === 1) ? Object.getOwnPropertyNames(o) : Array.splice.call(arguments, 1); props.forEach(function(prop) { if (!Object.getOwnPropertyDescriptor(o, prop).configurable) return; Object.defineProperty(o, prop, { writable: false, configurable: false }); }); return o; } //将o的指定名字的属性(或所有属性)设置为不可枚举的 function hideProps(o) { var props = (arguments.length === 1) ? Object.getOwnPropertyNames(o) : Array.splice.call(arguments, 1); props.forEach(function(prop) { if (!Object.getOwnPropertyDescriptor(o, prop).configurable) return; Object.defineProperty(o, prop, { enumerable: false }); }); return o; } /** * 将Range类的数据严格封装起来 * @param {Number} from * @param {Number} to */ function Range(from, to) { if (from > to) throw new Error("Range: from must be <= to"); function getFrom() { return from; } function getTo() { return to; } function setFrom(f) { if (f <= to) from = f; else throw new Error("Range: from must be <= to"); } function setTo(t) { if (f >= from) to = t; else throw new Error("Range: from must be <= to"); } Object.defineProperties(this, { from: { getFrom, setFrom, enumerable: true, configurable: false }, to: { getTo, setTo, enumerable: true, configurable: false } }); } Range.prototype = hideProps({ constructor: Range, includes: function(x) { return this.from <= x && x <= to; }, foreach: function(f) { for (var i = Math.ceil(this.from); i <= this.to; i++) f(i); }, toString: function() { return '(' + this.from + '...' + this.to + ')'; } });ES6的类语法实际上是ES5的语法糖
增加class关键词,用于定义类,跟C++/java的语法更相似,class定义的实际上还是一个函数;在类的内部有: constructor用来定义构造函数;未定义情况下,系统帮忙定义一个空的构造函数;内部定义的函数,作为类原型的方法;static关键词用来定义静态方法或静态数据(静态数据也可通过className.prop的方式定义);下面我们定义了一个简单的类
class Person { //constructor constructor(name, surname, age) { this.name = name; this.surname = surname; this.age = age; }; //Person.prototype.getFullName getFullName() { return this.name + " " + this.surname; }; //Person.older static older(person1, person2) { return (person1.age >= person2.age) ? person1 : person2; } } console.log("typeof Person:", typeof Person);//typeof Person: function继承有两个关键词extends和super:
child extends superclass:这是继承的基本语法super: 在构造函数内部使用super(args);用来调用父类的构造函数;super作为superclass.prototype的引用,用了调用父类的方法。下面是调用对上面类的继承,我们检验了super属性。
class PersonWithMiddlename extends Person { constructor(name, middlename, surname, age) { //类似于Person.call(this,name,surname,age);但有区别 super(name, surname, age); this.middlename = middlename; //证明super是superclass.prototype;但不可直接使用如super===Person.prototype是错的 console.log(super.getFullName === Person.prototype.getFullName);// } //重载方法 getFullName() { //调用父类的方法,只是为了演示 console.log("superclassMethod", super.getFullName()); return this.name + " " + this.middlename + " " + this.surname; } } var person = new PersonWithMiddlename('song', 'xiao', 'gao', 48); console.log("childclassMethod:", person.getFullName());//superclassMethod song gao //childclassMethod: song xiao gao下面的例子可以看出这种区别
/** * 原生构造函数的的this无法绑定,导致拿不到内部属性 */ function MyArray() { Array.apply(this, arguments); } MyArray.prototype = Object.create(Array.prototype); MyArray.prototype.constructor = MyArray; var colors = new MyArray(1); colors[2] = 'red'; //并未继承到Array的属性 console.log(colors.length, colors[2], colors);//0 red MyArray{'2','red'} /** * ES6允许继承原生构造函数,定义子类 * 因为ES6先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。 */ class MyArray_ES6 extends Array { constructor(...args) { super(...args); } } var colors = new MyArray_ES6(1); colors[2] = 'red'; //真正对内置类型Array的继承 console.log(colors.length, colors[2], colors);//3 red MyArray_ES6(3) [ <2 empty items>, 'red' ]声明:本文章实例主要来自《javascript权威指南(第六版)》和《ES6标准入门(第三版)》