作为一个iOS开发人员,至今记得公司刚开始让我写网页的时候,JavaScript无语法提示、不检查错误、不知如何在HTML里使用JavaScript等问题让我痛苦不堪,一度觉得这是一门很烂的语言,随着这两年在公司陆陆续续地写了一些网页,逐渐对JavaScript有了一些认识,发现JavaScript极其灵活,后来了解了一下node.js,才知道JavaScript已经不仅仅可以用来处理网页了,随着稍微再往深学一点,我发现JavaScript这门语言强大得惊人,一直对JavaScript没有系统地学过,以往一直都是边用边百度,在此,写此文目的,也是为了重新学习一遍JavaScript。
一、JavaScript一般可分为3个部分:
1. ECMAScript:JavaScript的语法核心;
2. DOM:文档对象模型,HTML各个节点,JavaScript可以操作DOM;
3. BOM:浏览器对象模型,JavaScript对浏览器进行操作;
本文只写JavaScript的语法核心的部分,接触了angularJS和node.js,发现学习这两门技术吃力的根本原因还是对JavaScript理解得太少导致的。
二、原始值和引用值
1. 原始值:存放在栈中的简单数据,如 var a = 3, b = a; 此时,a, b是两个独立的互不影响的变量,修改b的值不会影响到a,我们称 a, b 为原始值,在JavaScript中,原始值有5种:null,undefined,Number,Boolean,String。
2. 引用值:存放在堆内存的对象数据,如var obj = new Object(); 此时的obj本质上是一个指针,指向一个对象的堆内存地址。
var obj1 = new Object(); obj1.name = "zhang"; var obj2 = obj1; obj2.name = "li"; console.log(obj1.name); // "li"
以上例子中,obj2 与 obj1 两个指针都指向同一个对象,当修改obj2.name时,obj1.name 也会同时改变,这是需要注意的地方。
三、undefined与null
在JavaScript中,undefined并不表示未定义的对象,它只是代表该变量未初始化,而 null 则表示该变量为空,如:
var a; alert(a); // undefined alert(b); // 报错
当一个函数没有返回值时,返回值是undefined,如:
function test () { } alert(test()); // undefined
四、Object类
Object类本身很少用,但是如同Java一样,所有对象都继承自Object类,Object类有如下几个重要方法:
1. constructor 获取该对象的类型。
var c = ["red", "blue", "green"]; if (c.constructor == Boolean) { console.log("This is Boolean"); } else if (c.constructor == Object) { console.log("This is Object"); } else if (c.constructor == Array) { console.log("This is Array"); } // This is Array
2. prototype 该方法作用在类上面,指向一个类的所有属性,可以动态为类添加方法或属性。
// 定义一个Persion类 function Person(name) { this.name = name; } // 给Person类添加一个setAge方法 Person.prototype.setAge = function (age) { this.age = age; } // 给Persion类添加一个值 Person.prototype.finger = 10; var p = new Person("zhangsan"); p.setAge(25); console.log(p.age); // 25 console.log(p.finger); // 10 console.log(Person.prototype); // age方法,finger属性及Person类本身
prototype是一个非常强大的方法,他可以为任何类新增方法,也可以重写已有方法,为JavaScript提供无限可能!
3. hasOwnProperty 判断一个对象是否具有某个方法或属性。
var obj = new Object(); obj.test = function () { } obj.name = "zhang"; console.log(obj.hasOwnProperty('test')); // true console.log(obj.hasOwnProperty('name')); // true console.log(obj.hasOwnProperty('age')); // false
经测试,通过prototype添加的属性貌似不能用hasOwnProperty判断。
4. toString 将对象转为字符串。
var colors = ["red", "blue", "yellow"]; console.log(colors.toString()); // red,blue,yellow
五、其他常用运算符
1. instance 用来精准识别一个对象的类型。
var c = ["red", "blue", "green"]; console.log(c instanceof Array); // true console.log(typeof c); // Object
typeof具有局限性,很多情况下typeof返回一个Object,这个时候可以用instance判断,与前面的constructor类似。
2. delete 可以删除对象的方法或属性。
var p = new Object(); p.setName = function (name) { this.name = name; } p.setName("zhang"); delete p.setName; p.setName("li"); // 报错
delete只能删除开发者自定义的方法或属性,不能删除JavaScript的自带方法。
3. void 用于对任何返回结果都置为undefined,如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>JavaScriptTest</title> <script> function clickMe() { return "abc"; } </script> </head> <body > <a href="javascript:void(clickMe())">啊啊啊</a> </body> </html>
如果不加void,则点击之后链接会跳转,加上void,点击链接之后什么也不会发生。
六、JavaScript函数
(1)函数定义:JavaScript函数非常神奇,最常见的两种函数定义如下:
// 写法1 function func1 (arg1, arg2) { console.log("func1"); } // 写法2 var func2 = function (arg1, arg2) { console.log("func2"); }
以上两种函数定义方法略有区别:func1不管是写在前面还是后面,func1在脚本执行的时候会马上定义;而func2类似一个变量,一定要先定义func2,再执行func2(),否则会报错。
func1(); function func1 (arg1, arg2, arg3) { console.log("func1"); func3(); return; function func3() { // func3可以透过return定义,因为他只要一执行func1,func3马上被定义 console.log("func3"); } } func2(); var func2 = function (arg1, arg2) { console.log("func2"); } // func1 // func3 // 报错
(2)函数重载:JavaScript函数不能重载(所谓重载,也就是函数名称相同,函数的参数个数或参数类型不同),他会被后面的函数给覆盖。
function clickMe() { alert(100); } function clickMe(x) { alert(x); } clickMe(); // undefined
此处clickMe()里面并未传参,但是他执行的不是第一个函数,而是第二个。
(3)函数的arguments对象:
JavaScript函数中,有一个特殊的对象arguments,可以通过arguments获取函数的任何一个参数:
function func1 (x, y, z) { console.log("x = " + x + ", y = " + y + ", z = " + z); console.log("x = " + arguments[0] + ", y = " + arguments[1] + ", z = " + arguments[2]); } func1(3, 4, 5); // x = 3, y = 4, z = 5 // x = 3, y = 4, z = 5
arguments还可以用来检测函数的参数个数,arguments.length, 虽然func1的定义中是三个参数,但其由于没有重载,实可以传递更多或更少的参数,与其他的编程语言大不相同,JavaScript的自由度如此之大,是不是惊呆了!
(4)Function类
函数的定义方式除了上面介绍的两种,还有一种用类创建实例的方法创建一个函数:
var func1 = new Function("arg1", "arg2", "return arg1*arg2"); console.log(func1(3, 4)); // 12
由于是参数和函数体都是字符串的关系,写起来比较困难,读起来也不爽,所以实际上没见过有人用Function类来创建函数的,不过可以用这种写法来说明函数重载的问题。
var clickMe = new Function("alert(100)"); var clickMe = new Function("x", "alert(x)"); clickMe(); // undefined
现在知道为什么会执行第二个函数了,第一个函数被重写覆盖了。
(5)函数闭包
function add() { function doAdd (x, y) { return x + y; } var s = 0; for (var i = 0; i < arguments.length; i++) { s = doAdd(s, arguments[i]); } return s; } var sum = add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); console.log(sum); // 55
doAdd函数就是add函数的一个闭包,他在add函数里面,不受外界影响。
七、JavaScript对象
(1)JavaScript对象可以分为3种:
1. 本地对象 JavaScript语法本身提供的对象,如Object,Array,String,Boolean,Number,Date,Function等;
2. 内置对象 JavaScript有一个顶层的全局对象,在JavaScript中,实际上不存在独立的函数,所有的函数都必须是某一个对象的方法,如isNAN(),parseInt(),eval()等,我们在使用它们的时候并未写出其具体是那个对象调用的方法,他是属于全局对象的方法;另外还有Math对象,如Math.floor()等;
3. 宿主对象 JavaScript运行环境提供的对象,如DOM,BOM等。
(2)JavaScript对象的一些常用方法
Array对象:拼接 concat(),截取 slice(),入栈 push(),出栈 pop(),入队 unshift(),出队 shift(),逆序 reverse(),删除或替换 splice(),位置检索 indexOf();
String对象:拼接 concat(),截取 slice(),分割 split()。
内置对象提供了一个eval() 方法,这是一个解释程序,它可以把一段字符串解释成一段JavaScript代码,如:
var str = "function sayHi(s) {alert(s)}"; eval(str); sayHi("hello"); // 弹出 "hello"
再一次见证了JavaScript的灵活和强大!
(3)作用域
与其他的面向对象语言不一样,JavaScript中没有私有对象,任何对象的函数或属性都是公共的;
(4)关键字this
this总是指向调用该方法的对象。
function sepakLanguage() { alert('I speak ' + this.language); // 谁调用sepakLanguage,这个this就是谁 } var obj1 = new Object(); obj1.language = "Chinese"; obj1.sepak = sepakLanguage; obj1.sepak(); // I speak Chinese var obj2 = new Object(); obj2.language = "English"; obj2.sepak = sepakLanguage; obj2.sepak(); // I speak English
以上方法中,sepakLanguage中要引用对象的属性时,必须使用this关键字。
(5)设计模式
一般情况下创建一个对象最直接的方式如下:
var persion = new Object(); persion.name = "zhangsan"; persion.age = 25; persion.sex = "man"; persion.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } persion.showInfo(); // name:zhangsan, age:25, sex:man
但是如果有许多个persion对象要创建,那么这样写显得很啰嗦,于是人们就设计出了多种创建对象的模式:
1. 工厂模式:
function createPersion(name, age, sex) { var persion = new Object(); persion.name = name; persion.age = age; persion.sex = sex; persion.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } return persion; } var p1 = createPersion("zhang", 27, "women"); var p2 = createPersion("wang", 30, "man"); p1.showInfo(); // name:zhang, age:27, sex:women p2.showInfo(); // name:wang, age:30, sex:man
仔细琢磨后,我们发现每次创建一个persion对象都要新建一个showInfo()函数,很浪费内存,于是可以把showInfo()函数单独拿出来:
function showInfo() { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } function createPersion(name, age, sex) { var persion = new Object(); persion.name = name; persion.age = age; persion.sex = sex; persion.showInfo = showInfo; return persion; } var p1 = createPersion("zhang", 27, "women"); var p2 = createPersion("wang", 30, "man"); p1.showInfo(); // name:zhang, age:27, sex:women p2.showInfo(); // name:wang, age:30, sex:man
2. 构造函数模式
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); }; } var p1 = new Persion("zhang", 27, "women"); var p2 = new Persion("wang", 30, "man"); p1.showInfo(); // name:zhang, age:27, sex:women p2.showInfo(); // name:wang, age:30, sex:man
构造函数模式看上去和工厂模式很像,但构造函数模式是先定义一个类,类里面并不创建对象,使用时需要用这个类去new一个对象,当然,以上写法与工厂模式一样,仍然存在showInfo()会被创建多次的问题,仍然可以和工厂模式一样,把showInfo()提取出来,放到外面去。
3. 原型模式
在前面介绍的工厂模式和对象模式中,为了防止showInfo()被创建,我们把showInfo()提取成一个单独的函数放在外面,但是,这样很难表现出showInfo()是属于对象或类中的方法。前面我们介绍过Object类有一个prototype方法,该方法可以动态为类添加方法或属性,我们可以在构造函数的基础之上做一下改进:
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; } Persion.prototype.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } var p1 = new Persion("zhang", 27, "women"); var p2 = new Persion("wang", 30, "man"); p1.showInfo(); // name:zhang, age:27, sex:women p2.showInfo(); // name:wang, age:30, sex:man
通过prototype一举解决了showInfo()与类之间的关联问题!
4. 动态原型模式
有人指出,对于一个类来说,属性和方法都应该是在类的内部定义好,定义好了一个Persion类,又要在类的外部动态添加方法,这显得不和谐,但是,把方法定义在类内部,又会出现多次创建的问题,于是又有人提出动态原型模式:
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; if (typeof Persion.hadInitlized == "undefined") { Persion.prototype.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); }; Persion.hadInitlized = true; } } var p1 = new Persion("zhang", 27, "women"); var p2 = new Persion("wang", 30, "man"); p1.showInfo(); // name:zhang, age:27, sex:women p2.showInfo(); // name:wang, age:30, sex:man
把Persion.prototype.showInfo放进Persion类里面,并且通过给Persion一个标记,让Persion.prototype.showInfo只执行一次,就实现了把方法定义在内部的目的。
八、JavaScript的继承
继承是所有面向对象语言都有的一个特性,不过我断断续续写了将近两年的网页,至今都没用过继承,连JavaScript如何继承我都不知道,当我决定研究JavaScript继承的时候,我才知道JavaScript其实没有明确的继承机制,而是由开发者自己通过自己的手段实现的,所有的继承细节,都是有开发者自己决定!
先举个例子,一个父类为Persion,现在想定义一个Student类,该Student类想继承自Persion类,分析一下下面的代码是如何做到的:
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } } function Student(name, age, sex, grade) { this.Persion = Persion; this.Persion(name, age, sex); this.grade = grade; } var s1 = new Student("zhang", 27, "women", 3); console.log(s1); // Student {Persion: ƒ, name: "zhang", age: 27, sex: "women", grade: 3} s1.showInfo(); // name:zhang, age:27, sex:women
在子类中,极为巧妙地先给Student赋予一个Persion方法,然后调用Persion方法,就实现了对Persion的继承,接下来再给Student赋予自己的属性,或重写父类的方法,就这样实现了继承。
通过打印s1对象,我们发现s1中包含了一个Persion f对象,我们甚至可以直接通过 var p1 = new s1.Persion("xiaoming", 8, "man"); 又来创建一个persion对象,这显然不太合适,于是一般我们在继承完了之后,需要删除对Persion的引用。
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } } function Student(name, age, sex, grade) { this.Persion = Persion; this.Persion(name, age, sex); delete this.Persion; // 删除对父类的引用 // 新属性和新方法必须要在解除了对父类的引用后再定义,否则有可能会覆盖了父类的属性或方法,带来不可预知的错误,所以,上面那条delete语句也很有必要写上 this.grade = grade; } var s1 = new Student("zhang", 27, "women", 3); console.log(s1); // Student {name: "zhang", age: 27, sex: "women", grade: 3}
更加神奇的事情是,使用这种方法,可以轻松地实现多继承,比如有ClassA与ClassB,现在想定义一个类ClassC同时继承ClassA和ClassB,通过以下方法可以轻而易举地实现:
function ClassC() { this.ClassA = ClassA; this.ClassA(); delete this.ClassA; this.ClassB = ClassB; this.ClassB(); delete this.ClassB; }
太Amazing了!
由于以上继承方式的流行,JavaScript后来特意为Function对象添加了两个方法:call()和apply()。
先看一下这两个方法是干嘛的:
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } } var p = new Object(); Persion.call(p, "xiaohong", 10, "women"); console.log(p); // {name: "xiaohong", age: 10, sex: "women", showInfo: ƒ} p.showInfo(); // name:xiaohong, age:10, sex:women
call()是Function的方法,该函数的第一个参数是赋给this的对象,后面的参数是该函数本身的参数,大概描述一下call()函数的作用:把某个函数作用到某个对象上。
有了call方法,Student与Persion的继承关系可以写成:
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.showInfo = function () { console.log("name:" + this.name + ", age:" + this.age + ", sex:" + this.sex); } } function Student(name, age, sex, grade) { // this.Persion = Persion; // this.Persion(name, age, sex); // delete this.Persion; Persion.call(this, name, age, sex); // 前面三句可以使用call代替 this.grade = grade; } var s1 = new Student("zhang", 27, "women", 3);
apply()方法与call()方法差不多,只是把call()方法中的除this之外的参数写成数组的形式:
function Student(name, age, sex, grade) { Persion.apply(this, [name, age, sex]) this.grade = grade; }
除了通过方式实现继承,JavaScript还有一种有趣的继承方式,原型链:
function Persion() {} Persion.prototype.setName = function (name) { this.name = name; } Persion.prototype.setAge = function (age) { this.age = age; } Persion.prototype.setSex = function (sex) { this.sex = sex; } function Student() {} Student.prototype = new Persion(); Student.prototype.setGrade = function (grade) { this.grade = grade; } var s = new Student(); s.setName("xiaoming"); s.setAge(7); s.setSex("man"); s.setGrade(2); console.log(s);
原型链的有趣之处在于,它不需要将父类的属性一个一个赋给子类。但是要注意:Persion类是没有参数的,如果像这样写,是没有用的:
function Persion(name, age, sex) { this.name = name; this.age = age; this.sex = sex; } function Student() {} Student.prototype = new Persion(); Student.prototype.setGrade = function (grade) { this.grade = grade; } var s = new Student("xiaoming", 8, "man"); s.setGrade(2); console.log(s); // Student {grade: 2}, 只打印了grade,并没有继承到父类的属性
由此可见,JavaScript的继承完全由开发者自定义,当然还有更多的继承方式,在一般的网页开发里面,我是没有用遇到过继承这种需求的,我也只是为了研究一下JavaScript继承到底是怎么回事,只是肤浅地了解了一下,这里面学问太大了。
JavaScript的核心语法就写到这里,有时间没偷懒的话尽量再写一篇DOM和BOM相关的JavaScript应用吧。