JavaScript-基础2

最近参加了一次为时两天的 JavaScript 培训,回顾课程内容并进行整理,其中参考了如下两篇老师的文章。

JavaScript 编程技法入门
JS 编程道场

简介

函数是第一等公民。
JavaScript 的核心是 函数 和 对象 。

1
2
3
typeof Number // 'function'
typeof String // 'function'
typeof Function // 'function'

简化版 jquery 选择器实现

通过对 javascript 原生操作的封装,实现简化版的 jquery 类库。这里的代码并不复杂,个人感觉主要是要有类库的概念和实现思路,考虑将项目中使用的工具类逐步封装成类库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var $ = function(selector) {
var domObj = document.querySelector(selector);
var result = {
'0': domObj,
show: function() { domObj.style.display = 'none'; },
hide: function() { domObj.style.display = 'block'; },
append: function(str) {
domObj.innerHTML += str;
},
val: function(value) {
if (value === undefined) {
return domObj.value;
} else {
domObj.value = value;
}
},
on: function(action, callback) {
// 特定浏览器,这里是针对Chrome的写法
if (domObj.addEventListener) {
domObj.addEventListener(action, callback);
}
}
};
return result;
}

对象

js 是解释型动态语言,可随时添加属性或方法,很灵活。

1
2
3
4
5
var obj1 = new Object();
obj1.name = 'object1';
obj1.getName = function() {
return this.name;
};

推荐使用如下字面量形式的定义方式。

1
2
3
4
5
6
var obj2 = {
name: 'object2',
getName: function() {
return this.name;
}
};

遍历对象的属性和方法。

1
2
3
for(var key in obj2) {
console.log(key + '=' + obj2[key]);
}

函数

函数的三种定义方式如下:

1
2
3
4
5
6
7
8
9
10
// 匿名函数,通过一个变量来指向一个匿名函数,当这个函数不再需要时,JS的运行时环境会自动收集内存垃圾
var func1 = function(name) {
return 'hello,' + name;
};
// 具名函数,有变量提升,也就是在这段代码的前面是可以调用func2方法的
function func2 (name) {
return 'hello,' + name;
};
// 函数对象,这也表明了函数其实是Function类型的对象
var func3 = new Function('name', 'return "hello," + name');

函数体的内部只有在函数被调用时才会进入,下面 demo 的运行结果是先后输出 1、2、3、4、5。使用 Chrome 调试断点进入时,打开 Sources 注意观察 Scope 的变化,可以看到运行到 func1()时 Scope 只有 Globle,当进入到调用的方法时,会有 Local,里面是函数内部定义的变量或方法,当函数内部用到了外面的变量时,会有 Closure,里面是引用的外部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(1);
var func1 = function() {// 进来之后有Local
console.log(3);
var a = 0;
function func2() { // 进来之后有Closure
console.log(5);
var f = a + 5;
}
console.log(4);
func2();
}
console.log(2);
debugger;
func1(); // 这时只有Global

作用域

this 取决于在哪调用,而不是在哪定义,通过“对象.”调用的,this 都是这个对象,直接调用的 this 都是 window。

1
2
3
4
5
6
7
8
9
10
11
var func = function(name) {
this.name = name;
};
func('jobs'); // 进入func,this是window
var o = {
setName: func
};
o.setName('jobs'); // 进入func,this是o
var method = o.setName;
method('jobs'); // 进入func,this是window
var obj = new func('jobs');// 进入func,this是obj

通过函数的 call、apply、bind 方法进行调用可改变作用域,call 和 apply 只是传递参数的方式不同。

1
2
3
4
var o = {};
func.call(o, 'jobs'); // 绑定作用域为o
func.apply(o, ['jobs']);
func.bind(o)('jobs');

模拟类

函数名的第一个字母大写,表示是用来构造对象的,不是用来执行的。

用方式一模拟类,o1 和 o2 有自己的 name 属性,但第三个输出结果为 false,这说明 o1.sayHello 和 o2.sayHello 指向不同内存,这样存在内存的浪费。

1
2
3
4
5
6
7
8
9
10
11
// 方式一
var Person = function(name) {
this.name = name;
this.sayHello = function() {};
};
var o1 = {}, o2 = {};
Person.call(o1, 'jobs');
Person.call(o2, 'gates');
console.log(o1.name); // 输出jobs
console.log(o2.name); // 输出gates
console.log(o1.sayHello === o2.sayHello); // 输出false

用方式一模拟类,第三个输出结果为 true,这时不存在内存浪费了,但破坏了封装,在外部也可以调用 sayHello 方法。

1
2
3
4
5
6
7
8
9
10
11
12
// 方式二
function sayHello() {}
var Person = function(name) {
this.name = name;
this.sayHello = sayHello;
};
var o1 = {}, o2 = {};
Person.call(o1, 'jobs');
Person.call(o2, 'gates');
console.log(o1.name); // 输出jobs
console.log(o2.name); // 输出gates
console.log(o1.sayHello === o2.sayHello); // 输出true

用方式三模拟类,通过即时函数封装作用域后,可以比较好的模拟类了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方式三
var Person = function() {
// private,在外部无法直接调用
function sayHello() {}
var Person = function(name) {
this.name = name;
this.sayHello = sayHello;
};
// public
return Person;
}(); // 即时函数,封装作用域
var o1 = {}, o2 = {};
Person.call(o1, 'jobs');
Person.call(o2, 'gates');
console.log(o1.sayHello === o2.sayHello); // 输出true

用方式四模拟类,在即时函数封装作用域的基础上,又添加了命名空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方式四
// 模仿命名空间,可封装类库给别人使用
var cplh = cplh || {};
// 模仿封装
(function(g) {
// 模仿private,没有加到Person上
function sayHello() {}
var Person = function(name) {
this.name = name;
this.sayHello = sayHello;
};
// 模仿public
g.Person = Person;
})(cplh); // 加上括号来说明这是一个表达式
// 模仿初始化
var o1 = {}, o2 = {};
cplh.Person.call(o1, 'jobs');
cplh.Person.call(o2, 'gates');
console.log(o1.sayHello === o2.sayHello); // 输出true

原型

所有对象都有原型对象__proto__,只有函数对象有原型属性prototype,当这个函数被用作构造函数来创建实例时,该函数的原型属性(prototype)将被作为原型赋值给所有对象实例,也就达到了重用方法的作用,即所有实例的原型引用的是函数的原型属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var i = 5;
var str = 'abc';
var Person = function() {};
var person = new Person();
// 所有对象都有原型对象
i.__proto__ // 有 [Number: 0]
str.__proto__ // 有 [String: '']
Person.__proto__ // 有 [Function]
// 只有函数对象有原型属性
i.prototype // undefined
str.prototype // undefined
Person.prototype // 有 [Object]
// 所有实例的原型引用的是函数的原型属性
console.log(i.__proto__ === Number.prototype) // 输出true
console.log(str.__proto__ === String.prototype) // 输出true
console.log(person.__proto__ === Person.prototype) // 输出true

在执行完 var Person = function() {}; 语句时,会分配三块内存,Person 变量的内存、function 函数的内存、protptype 指向对象的内存,这个对象的 constructor 又指向 Person 指向的函数。

1
2
3
4
var Person = function() {};
console.log(Person);
console.log(Person.prototype); // 输出一个Object,有constructor属性
console.log(Person.prototype.constructor === Person); // true

JavaScript 是动态语言,取决于运行时,不是定义时,因此下面的代码中示例一的对象 o 是有 sayHello 方法的。通过如示例二、示例三、示例四的方式给 Function、String、jquery 等全局函数的 prototype 添加方法来实现共有的新功能。

1
2
3
4
5
6
// 示例一
var o = {};
o.__proto__ = Person.prototype;
console.log(o instanceof Person); // 输出 true
Person.prototype.sayHello = function() {};
console.log(o.sayHello); // 输出 function() {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 示例二
Function.prototype.method = function(name,fn) {
this.prototype[name] = fn;
return this;
};
function Person (name) {
this.name = name;
};
Person.method('sayHello',function() {
return 'Hello,' + this.name;
}).method('show',function() {
return 'show';
});
var jobs = new Person('jobs');
console.log('Hello,jobs' === jobs.sayHello()); // 输出 true
console.log('show' === jobs.show()); // 输出 true
1
2
3
4
5
6
7
8
9
10
// 示例三
String.prototype.fill = function(id){
// var sql = "select * from " + this;
$.getJSON('/' + this, null functon(json, textStatus){
var data = JSON.parse(json);
$(id).append(data);
});
};
// 给id为wellTable01的元素添加从wells表中或接口中查出的数据
"wells".fill('#wellTable01');
1
2
3
4
5
6
7
// 示例四
(function(){
var myMethod = function() {
console.log('自定义方法');
};
$.prototype.myMethod = myMethod;
})();

实现类

利用原型实现类,注意下面方式一到方式四的区别,方式一到方式三的 o 对象已经有了 sayHello 方法,达到了重用方法的目的,但使用了__proto__,理论上 JavaScript 中是不让用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式一
// 构造函数
var Person = function(name) {
this.name = name;
};
// 方法体
var proto = {
sayHello: function(){}
};
// 创建对象
var o = {};
// 赋值方法
o.__proto__ = proto;
// 初始化
Person.call(o, 'jobs');
console.log(o.sayHello);

方式二只是将 proto 作为 Person 的属性使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式二
// 构造函数
var Person = function(name) {
this.name = name;
};
// 方法体
Person.proto = {
sayHello: function(){}
};
// 创建对象
var o = {};
// 赋值方法
o.__proto__ = Person.proto;
// 初始化
Person.call(o, 'jobs');
console.log(o.sayHello);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式三
// 构造函数
var Person = function(name) {
this.name = name;
};
// 方法体
Person.prototype = {
sayHello: function(){}
};
// 创建对象
var o = {};
// 赋值方法
o.__proto__ = Person.prototype;
// 初始化
Person.call(o, 'jobs');
console.log(o.sayHello);
1
2
3
4
5
6
7
8
9
10
11
// 方式四
// 构造函数
var Person = function(name) {
this.name = name;
};
// 方法体
Person.prototype = {
sayHello: function(){}
};
var o = new Person('jobs');
console.log(Person.prototype.sayHello);

方式四使用 new 创建的对象,也就是说函数 Person 被用作构造函数来创建实例了,这时该函数 prototype 将被作为原型__proto__赋值给对象的实例,也就相当于如下代码。注意当函数名的第一个字母大写时,表示是用来构造对象的,不是用来执行的,该函数的 prototype 是给构造出来的对象用的,函数自己用不了。

1
2
3
4
5
6
7
// var o = new Person('jobs'); 等同于以下代码
// 创建对象
var o = {};
// 赋值方法
o.__proto__ = Person.prototype;
// 初始化
Person.call(o, 'jobs');

当定义一个当函数被用作构造函时,这个函数会默认在函数体开头和结尾添加如下代码,如果函数 return 的不是对象,在使用 new 实例化对象时这个 return 语句会被忽略,因为需要返回一个对象。

1
2
3
4
5
6
7
8
9
10
function Class() {
// 默认添加
var this = {};
...
// 会被忽略,因为需要返回对象
return 1;
...
// 默认添加
return this;
}

实现继承

封装自己的框架,要有类、方法、复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function Class(options) {
var Klass = function() {
// 如果Class有初始化
if(typeof this.initialize === 'function'){
this.initialize.apply(this, arguments);
}
};
for(var attr in options) {
Klass.prototype[attr] = options[attr];
}
Klass.extends = function(proto) {
// 定义子类
var Child = function() {
Klass.apply(this, arguments);
};
// var f = {};
// f.__proto__ = this.prototype;
// // 相当于上面的,不能使用__proto__
// var f = new Klass();
// Child.prototype = f;
// 继承基类
// 不能使用 Child.prototype = this.prototype; 这样extends添加的方法会影响到基类
var F = function() {
/*
这是定义F函数的原因,如果不显示定义constructor的指向,constructor将指向Object,通过constructor已经无法确定对象的类型了。
这样显示定义constructor属性的在实例中是可枚举的。
*/
this.constructor = Child;
};
F.prototype = this.prototype;// 继承
var f = new F();
Child.prototype = f;
// 类的实例方法
for(var attr2 in proto) {
Child.prototype[attr2] = proto[attr2];
}
return Child;
};
return Klass;
};
// 方式一
var Person1 = new Class();// 函数对象可以执行
var jobs1 = new Person();
// 方式二
var Person2 = new Class({
show: function() {}
});
var jobs2 = new Person();
// 方式三
var Person3 = new Class({
show: function() {}
});
var Actor = Person3.extends({
sayHello: function(){}
});
var jobs3 = new Actor();

封装商业框架,有 Model 和 View。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var Adobe = (function() {
var Model = function() {};
// 实例方法
Model.prototype = {
constructor: Model,
get: function() {},
set: function() {}
}
// 绑定作用域,在context作用域上执行func函数
function proxy (func, context) {
return function() {
func.apply(context, arguments);
};
}
var View = function () {
//Hack: 绑定事件,这段代码不明白
var events = this.events || {};
for(var item in events){
var func = proxy(this[events[item]], this);
var reg = /(.+)\s+(.+)/;
var result = reg.exec(item);
$(result[2]).bind(result[1], func);
}
};
View.extend = Model.extend = function(options) {
var Parent = this;
// 构造函数
var Klass = function() {
if(typeof this.initialize === 'function'){
this.initialize.apply(this, arguments);
}
Parent.apply(this, arguments);
};
// 继承
var F = function() {
this.constructor = Klass;
};
F.prototype = this.prototype;
Klass.prototype = new F();
Klass.prototype.__super__ = this.prototype;
// 扩展
for(var name in options){
Klass.prototype[name] = options[name];
}
return Klass;
};
return {
Model: Model,
View: View
}
})();
// 使用代码
var Todo = Adobe.Model.extend({
initialize: function(title) {
if(typeof title === 'string'){
this.set({title: title});
}
}
});
var todo = new Todo('Test');
console.log(typeof todo.get === 'function');
console.log(typeof todo.set === 'function');
console.log(typeof todo.initialize === 'function');

练习

1
2
3
4
5
6
7
8
9
10
11
function fun() {}
var f = new fun();
f.__proto__; // fun.prototype
f.__proto__.__proto__; // Object.prototype
f.__proto__.__proto__.__proto__; // null
f.__proto__.__proto__.__proto__.__proto__; // 报错
fun.__proto__; // Function.prototype
fun.__proto__.__proto__; // Object.prototype
f.prototype; // undefined
fun.prototype; // { constructor: fun }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Demo {
a = 1;
m1() {}
m2 = () => {};
static s2 = 2;
}
// 等同写法
function Demo() {
this.a = 1;
this.m2 = () => {};
}
Demo.prototype.m1 = function () {};
Demo.s2 = 2;