JavaScript 进阶

目标

JavaScript

JavaScript 已经是当今世界最流行的编程语言之一

JavaScript : 世界上被误解最多的编程语言

关于名字 - JavaScript

JavaScript 跟 Java 没有关系!
JavaScript 是一门解释性脚本语言
与 C 相比,它牺牲了性能,以提升动态特性和开发效率

JavaScript 被定型

它只能在Web浏览器里跑?

JavaScript 有设计上的失误

没有一门编程语言是完美的

JavaScript 是面向对象的?

它连 class 都没有!

JavaScript 特性

类似C的结构化编程

// 条件语句
if (lilei.money >= 1000000 && lilei.house >= 1) {
	lilei.marry(hanmeimei);
} else {
	lilei.dieAlone();
}

// 循环语句
while (lilei.money < 1000000 && lilei.house === 0) {
	lilei.writeCodes();
}

// 选择语句
switch (lilei.workHoursPerDay) {
	case 0: case 1: /* ... */ case 7:
		company.fire(lilei);
		break;
	case 8:
		company.pay(lilei, 3000);
		break;
	case 9:
		company.pay(lilei, 4000);
		break;
	/* ... */
	case 24:
		lilei.die();
}

动态

动态类型

typevalue 关联,而不是与 variable

var foo = 1;
foo = "A string";
foo = [1, 2];
foo = new Date();
foo = function(){};

Eval

在运行时,将给定的字符串当作代码语句执行

<script>
var func = <?php echo json_encode($user_send['func']); ?>;
eval(func + "()");
function sayHello() {}
function sayGoodbye() {}
</script>

对象动态更新

对象的属性和方法都可以在运行时,增加、修改、删除

var obj = {
	prop: "value"
};

// 增加方法
obj.other = function(){};
// 修改属性
obj.prop = "new value";
// 删除属性
delete obj.prop;

反射

var obj = {
	foo: "value",
	bar: function() {
		return this.foo;
	}
};

// 使用字符串访问对象的属性或方法
console.log(obj.foo === obj["foo"]);

// 遍历对象的属性和方法
for (var key in obj) {
	console.log(key, obj[key]);
}

函数式编程

First-class function

函数是 JavaScript 中的一等公民

函数可以做哪些事?

var bind = function(func, target) {
	return function() {
		func.apply(target, arguments);
	};
};

var utils = {
	trim: function(str) {
		return str.replace(/^\s+|\s+$/g, "");
	}
};

可变参数

函数的参数数量没有限制

可以通过参数名或者 arguments 对象来访问参数

(function(){
	// `arguments` 拥有数组的外观,可以通过数字下标访问各个参数
	var arg1 = arguments[0], // 1
		arg2 = arguments[1]; // 2

	// 但它并不是数组
	console.log(arguments instanceof Array); // false
	console.log(arguments.slice); // undefined

	// 对 `arguments` 进行数组相关操作
	var argElse = Array.prototype.slice.call(arguments, 2);
	console.log(argElse); // [3, 4]
})(1, 2, 3, 4);

作用域

C语言中的 block scope :

int function foo(void) {
	int bar = 1;
	{
		int bar = 2;
	}
	return bar; // 1
}

JavaScript 中的 function scope :

function foo() {
	var bar = 1;
	{
		var bar = 2;
	}
	return bar; // 2
}

作用域链 Scope chain

访问一个变量时,会从本地变量和参数开始,逐级向上遍历作用域直到 global scope

var scope = 0, zero = "global-scope";
(function(){
	var scope = 1, one = "scope-1";
	(function(){
		var scope = 2, two = "scope-2";
		(function(){
			var scope = 3, three = "scope-3";
			// scope-3 scope-2 scope-1 global-scope
			console.log([three, two, one, zero].join(" "));
			console.log(scope); // 3
		})();
		console.log(typeof three); // undefined
		console.log(scope); // 2
	})();
	console.log(typeof two); // undefined
	console.log(scope); // 1
})();
console.log(typeof one); // undefined
console.log(scope); // 0

闭包 Closure

在一个函数中,定义另一个函数,称为函数嵌套。函数的嵌套将形成一个闭包。

function bind(func, target) {
	return function() {
		func.apply(target, arguments);
	};
}

如何理解闭包?

var scope = 0, zero = "global-scope";
(function(){
	var scope = 1, one = "scope-1";
	(function(){
		var scope = 2, two = "scope-2";
		(function(){
			var scope = 3, three = "scope-3";
			// scope-3 scope-2 scope-1 global-scope
			console.log([three, two, one, zero].join(" "));
			console.log(scope); // 3
		})();
		console.log(typeof three); // undefined
		console.log(scope); // 2
	})();
	console.log(typeof two); // undefined
	console.log(scope); // 1
})();
console.log(typeof one); // undefined
console.log(scope); // 0

如何理解闭包?

function bind(func, target) {
	return function() {
		func.apply(target, arguments);
	};
}

function foo(bar) {
	console.log(this.count += bar);
}

var counterA = { count: 0 };
var counterB = { count: 0 };

var increaseA = bind(foo, counterA);
var increaseB = bind(foo, counterB);

increaseA(1); // 1
console.log(counterA.count); // 1
console.log(counterB.count); // 0

increaseB(2); // 2
increaseB(3); // 5
console.log(counterA.count); // 1
console.log(counterB.count); // 5

JavaScript 是面向对象的

JavaScript : 基于原型的面向对象

JavaScript 中没有 classfunctionclass

function Person() { }

对象 - 类的实例

使用 new Func 将创建一个 Func 的实例

function Person() { }
var lilei = new Person();
var hanmeimei = new Person();

构造器

functionconstructor

function Person() {
	console.log("A person is born");
}
var lilei = new Person();
console.log(lilei.constructor === Person); // true

属性和方法

通过关键字 this 或构造器(函数)的 prototype 来设置类的属性和方法

function Person(gender) {
	this.gender = gender || "unkown";
}

Person.prototype.sayHello = function() {
	console.log("Hello world!");
};

var lilei = new Person("male");
console.log(lilei.gender);
lilei.sayHello();

继承

通过将子类的 prototype 赋值为父类的一个实例,来实现继承

function Student() {
	Person.call(this);
}

Student.prototype = new Person();
// Student.prototype = Object.create(Person.prototype);

Student.prototype.constructor = Student;

Student.prototype.sayHello = function() {
	console.log("Hello school!");
};

Student.prototype.sayGoodbye = function() {
	console.log("Goodbye school!");
};

var lilei = new Student();

lilei.sayHello();
lilei.sayGoodbye();

console.log(lilei instanceof Person);  // true
console.log(lilei instanceof Student); // true

基于原型的继承是如何实现的?

原型链 Scope chain

访问一个对象的属性时,会先从自身属性开始,向上追溯原型链
直到找到这个属性,或者遇到 null 为止

// lilei --> student --> person --> Object.prototype --> null

var person = {
	gender: "unkown",
	sayHello: function() {
		console.log("Hello world!");
	},
	walk: function() {
		console.log("I am walking");
	}
};

var student = Object.create(person);

student.sayHello = function() {
	console.log("Hello school!");
};
student.study = function() {
	console.log("I am studying!");
};

var lilei = Object.create(student);
lilei.gender = "male";

console.log(lilei.gender);
lilei.walk();
lilei.sayHello();
lilei.study();

console.log(person.gender);
delete lilei.gender;
console.log(lilei.gender);
console.log(person.gender);

// lilei --> student --> person --> Object.prototype --> null

console.log(student.isPrototypeOf(lilei)); // true
console.log(person.isPrototypeOf(student)); // true
console.log(Object.prototype.isPrototypeOf(person)); // true
console.log(null === Object.prototype.__proto__); // true

小心,JavaScript 陷阱重重

+ 加号

作为二元运算符时,它既是数学运算的加法,也是字符串的拼接

console.log(   1 + 2   ); // 3
console.log( "3" + "4" ); // "34"
// 2
console.log(   1 + "3" );
console.log( "3" + 1   );
// 3
console.log( 1 + null      );
console.log( 1 + undefined );
console.log( 1 + NaN       );
// 4
console.log( "3" + null      );
console.log( "3" + undefined );
console.log( "3" + NaN       );
// 5
console.log( 1 + {} );
console.log( 1 + [] );
// 6
console.log( "3" + {} );
console.log( "3" + [] );

+ 加号

如何决定一段代码中的 + 是数学运算还是字符串拼接?

a + b:
	pa = ToPrimitive(a)
	pb = ToPrimitive(b)
	if (pa is string || pb is string)
		return concat(ToString(pa), ToString(pb))
	else
		return add(ToNumber(pa), ToNumber(pb))

with

将一个语句块的上下文绑定为一个指定对象

with (document) {
	write("foo");
	getElemntById("bar").innerHTML = "foobar";
	alert("Hello world!");
}

// document.write("foo");
// document.getElemntById("bar").innerHTML = "foobar";
// window.alert("Hello world!");

with

不推荐使用 with 语句

with (document) {
	write("foo");
	getElemntById("bar").innerHTML = "foobar";
	alert("Hello world!");
}

// document.write("foo");
// document.getElemntById("bar").innerHTML = "foobar";
// window.alert("Hello world!");

; 分号自动插入

在语句结束时,你不必手动输入分号,换行即可

function foo() {
	var bar = "value"
	return bar
}

// `{}` 包围的语句块的最后一个语句的分号也可省略
function bar() { return "foo" }

; 分号自动插入

过于依赖分号自动插入,将带来一些潜在问题

function foo() {
	return
	{
		bar: 1
	}
}

function bar() {
	var a, b, c, d, e
	a = b + c
	(d + e).toString()

	// a = b + c(d + e).toString();
}

声明提升

看看这段代码,我们将得到什么结果?

var foo = 1;
function bar() {
	// 这个条件成立吗?
	if (! foo) {
		var foo = 10;
	}
	alert(foo);
}
bar();

声明提升

那么这段代码呢?

var a = 1;
function b() {
	a = 10;
	return;
	function a() {}
}
b();
alert(a);

声明提升

变量、函数的声明将提升到当前函数主体的顶部
不管这个声明语句是否出现在了不可到达的地方

var foo = 1;
function bar() {
	// 这个条件成立吗?
	if (! foo) {
		var foo = 10;
	}
	alert(foo);
}
bar();

var a = 1;
function b() {
	a = 10;
	return;
	function a() {}
}
b();
alert(a);
var foo = 1;
function bar() {
	var foo;
	if (! foo) {
		foo = 10;
	}
	alert(foo);
}
bar();

var a = 1;
function b() {
	function a() {}
	a = 10;
	return;
}
b();
alert(a);

eval

eval 是 JavaScript 的动态特性之一

<script>
var func = <?php echo json_encode($user_send['func']); ?>;
eval(func + "()");
function sayHello() {}
function sayGoodbye() {}
</script>

eval

不推荐使用 eval

多行字符串

var multiStr = "this is a multi-line string, \
and this is the second line. \
yes, the string ends here";

多行字符串

假设我们后期做一些维护

var multiStr = "this is a multi-line string, \
and this is the second line. \ 
now i want to insert a line right here, \
yes, the string ends here";

变量泄露

function foo() {
	var type = "first";

	// do some jobs

	typo = "second";

	// do some else

	return type;
}

foo();

arguments.callee

递归函数

function factorial(n) {
	return n <= 1 ? 1 : factorial(n - 1) * n;
}
[1, 2, 3, 4, 5].map(factorial);

arguments.callee

不想污染命名空间,匿名函数如何递归?

[1, 2, 3, 4, 5].map(function(n) {
	return n <= 1 ? 1 : /* what goes here? */ (n - 1) * n;
});

arguments.callee

我们有 arguments.callee

[1, 2, 3, 4, 5].map(function(n) {
	return n <= 1 ? 1 : arguments.callee(n - 1) * n;
});

arguments.callee

不推荐使用 arguments.callee

arguments.callee

命名函数表达式

[1, 2, 3, 4, 5].map(function factorial(n) {
	return n <= 1 ? 1 : factorial(n - 1) * n;
});

JavaScript 陷阱

代码质量

JavaScript 有太多的语法陷阱
如何避免?
JavaScript 灵活、开发效率高,我们可以非常轻易地实现大多数需求
如何保证代码质量?

严格模式

function foo() {
	"use strict";

	// now foo is in strict mode

	function bar() {
		// in strict mode too
	}
}

严格模式

严格模式做些什么?

严格模式不是万能的

代码质量工具

我们需要一款优秀的代码质量工具

来检测我们的 JavaScript 代码中的错误和潜在问题

JSHint

JSHint 是由 JSLint 分叉(fork)而来的,是社区推动

( JSLint 的作者是著名 JS 权威 Douglas Crockford )

JSHint

JSHint 本身使用 JavaScript 开发,
因此既可以运行在浏览器中,也可以运行在 Node.js 平台

JSHint

强制选项示例:

选项 说明
curly 循环和条件语句块,始终带上 `{ }`
eqeqeq 禁止使用 ==!=,而应使用 ===!==
es3 使用 ECMAScript 3 规范,以兼容老浏览器 IE 6/7/8
forin 要求在 forin 循环里使用 `hasOwnProperty()` 过滤属性
latedef 禁止在一个变量声明前使用它
noarg 禁止使用 arguments.callerfunc.caller
quotmark 保证引号一致性
undef 禁止使用明显未声明的变量
strict 要求所有函数运行在 ECMAScript 5 的严格模式下
trailing 禁止行尾空白
maxdepth 控制代码块的最大嵌套深度
maxstatements 控制每个函数的最大语句数

JSHint

松散选项示例:

选项 说明
asi 允许分号缺失
boss 允许在期望是比较运算的地方使用赋值表达式
evil 允许使用 eval
multistr 允许多行字符串(但仍会提示将导致错误的行尾空白)

单元测试

QUnit

QUnit 最初是著名JS库 jQuery 的一部分,
后来独立了出来,作为一个JS单元测试框架

QUnit 示例

test("traps of +", function() {
	expect(14);

	strictEqual(1 + 2, 3);
	strictEqual("3" + "4", "34");

	strictEqual(1 + "3", "13");
	strictEqual("3" + 1, "31");

	strictEqual(1 + null, 1);
	ok(isNaN(1 + undefined));
	ok(isNaN(1 + NaN));

	strictEqual("3" + null, "3null");
	strictEqual("3" + undefined, "3undefined");
	strictEqual("3" + NaN, "3NaN");

	strictEqual(1 + {}, "1[object Object]");
	strictEqual(1 + [], "1");

	strictEqual("3" + {}, "3[object Object]");
	strictEqual("3" + [], "3");
});

自动任务工具

Grunt

"grunt" 的意思是 "打呼噜",其目标就是让开发者们一劳永逸

开始使用 Grunt

Grunt 运行在 Node.js 平台,Grunt 及其插件都通过 npm 安装和管理

Grunt

Gruntfile.js 示例

var fs = require("fs");

function htmlspecialchars(str) {
	return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function replaceJshintComments(str) {
	return str.replace(/[ \t]*\/\*\s*(jshint|global)\s+[\s\w\,\:\-]+\*\/(?:\r?\n|\r)/, "");
}

module.exports = function(grunt) {

	// 幻灯片名称列表
	var slideList = [
		"cover",
		"current", /*"popular",*/ "misunderstood",
		"feature", "structured", "dynamic", "functional", "scope", "closure",
		"object-oriented", "class", "instance", "constructor", "property", "inheritance",
		"traps", "quality", "test", "automation",
		"thanks"
	];

	// 遍历获取幻灯片文件名
	var slides = [];
	slideList.forEach(function(name){
		slides.push("slides/" + name + ".html");
	});
	slides.unshift("src/intro.html");
	slides.push("src/outro.html");

	// 遍历获取示例代码文件内容
	var codes = {};
	var codeDirs = ["codes", "bad-codes"];
	codeDirs.forEach(function(dir){
		var codeList = fs.readdirSync(dir);
		codeList.forEach(function(name){
			var key = name.replace(/\.[^\.]+/, "");
			codes[key] = replaceJshintComments(htmlspecialchars(fs.readFileSync(dir + "/" + name).toString("utf8")));
		});
	});

	codes.Gruntfile = htmlspecialchars(fs.readFileSync("Gruntfile.js").toString("utf8"));

	// 配置
	grunt.initConfig({
		// pkg: grunt.file.readJSON("package.json"),
		codes: codes,
		concat: {
			options: {
				separator: "\n\n",
				process: true
			},
			dist: {
				// src: ["src/intro.html", "slides/cover.html", "src/outro.html"],
				src: slides,
				dest: "dist/index.html"
			}
		},
		jshint: {
			options: {
				"jshintrc": ".jshintrc"
			},
			dist: ["Gruntfile.js", "dist/main.js", "codes/*.js"]
		},
		uglify: {
			impress: {
				src: "dist/impress.js",
				dest: "dist/impress.min.js",
			}
		},
		qunit: {
			all: ["test/*.html"]
		}
	});

	// 载入任务
	grunt.loadNpmTasks("grunt-contrib-concat");
	grunt.loadNpmTasks("grunt-contrib-jshint");
	grunt.loadNpmTasks("grunt-contrib-uglify");
	grunt.loadNpmTasks("grunt-contrib-qunit");

	// 注册任务
	grunt.registerTask("default", ["jshint", "concat:dist"]);

	grunt.registerTask("impress", ["uglify:impress"]);
	grunt.registerTask("test", ["qunit"]);
};

Grunt

Grunt 使用 Node.js 实现
配合丰富的任务插件,Grunt可以将许多的开发任务自动化

关于本演示文稿

源代码托管在 https://github.com/weareoutman/advancedJSGuide
在线演示版本 http://weareoutman.github.io/advancedJSGuide/
在线普通版本 http://weareoutman.github.io/advancedJSGuide/?impress=no
 
推荐阅读 https://github.com/dypsilon/frontend-dev-bookmarks

谢谢!