高级函数

安全类型的检测

例如:

1
2
3
4
5
let isArray = value => Object.prototype.toString.call(value) === '[object Array]'
let isFunction = value => Object.prototype.toString.call(value) === '[object Function]'
let isRegExp = value => Object.prototype.toString.call(value) === '[object RegExp]'
// let isNativeJSON = value => window.JSON && Object.prototype.toString.call(value) === '[object JSON]'
// 上面的方法有疑问?什么是原生JSON?

作用域安全的构造函数

例如以下的构造函数:

1
2
3
function Person(name){
this.name = name
}

如果我们不是new出来的,而是直接将构造函数当做一个普通的函数调用就会将name属性绑定到window上面(this指向window):

1
2
Person(123)
window.name // 123

以上的操作为window添加了新的属性,导致window的属性意外增加或者覆盖!解决方案是创建一个作用域安全的构造函数:

1
2
3
4
5
6
function Person(name){
if(this instanceof Person)
this.name = name
else
return new Person(name)
}

以上的函数要么使用现有的实例,要么创建新的实例,this指向的始终是实例,避免了在全局对象上意外增加属性.这是最佳实践.

js中的类和继承

类定义的方法

构造函数定义类

1
2
3
4
5
6
7
8
9
function Person(){
this.name = 'michaelqin';
}
Person.prototype.sayName = function(){
alert(this.name);
}

var person = new Person();
person.sayName();

对象创建方法定义类

1
2
3
4
5
6
7
var Person = {
name: 'michaelqin',
sayName: function(){ alert(this.name); }
};

var person = Object.create(Person);
person.sayName();

ES5推荐使用第二种模式,但是原型法更普遍。

类继承的方法

原型链

1
2
3
4
5
6
7
8
9
10
function Animal() {
this.name = 'animal';
}
Animal.prototype.sayName = function(){
alert(this.name);
};

function Person() {}
Person.prototype = Animal.prototype; // 人继承自动物
Person.prototype.constructor = 'Person'; // 更新构造函数为人

属性复制

1
2
3
4
5
6
7
8
9
10
11
12
13
function Animal() {
this.name = 'animal';
}
Animal.prototype.sayName = function() {
alert(this.name);
};

function Person() {}

for(prop in Animal.prototype) {
Person.prototype[prop] = Animal.prototype[prop];
} // 复制动物的所有属性到人量边
Person.prototype.constructor = 'Person'; // 更新构造函数为人

ES5中的继承是通过借用父类的构造方法来实现方法/属性的继承:

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
// 父类
function supFather(name) {
this.name = name;
this.colors = ['red', 'blue', 'green']; // 复杂类型
}

supFather.prototype.sayName = function (age) {
console.log(this.name, 'age');
};

// 子类
function sub(name, age) {
// 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
supFather.call(this, name);
this.age = age;
}

// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}

inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
console.log(this.age, 'foo');
};

// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')

console.log(instance1);
//>> {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2);
//>> {"name":"小明","colors":["red","blue","green"],"age":18}

js中多重继承的实现就是使用属性复制来实现的,因为当父类的prototype属性被复制后,子类自然拥有类似的行为和属性。

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
const mixinClass = (base, ...mixins) => {
const mixinProps = (target, source) => {
Object.getOwnPropertyNames(source).forEach(prop => {
if (/^constructor$/.test(prop)) { return; }
Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
})
};

let Ctor;
if (base && typeof base === 'function') {
Ctor = class extends base {
constructor(...props) {
super(...props);
}
};
mixins.forEach(source => {
mixinProps(Ctor.prototype, source.prototype);
});
} else {
Ctor = class {};
}
return Ctor;
};

class A {
methodA() {}
}
class B {
methodB() {}
}
class C extends mixinClass(A, B) {
methodA() { console.log('methodA in C'); }
methodC() {}
}

let c = new C();
c instanceof C // true
c instanceof A // true
c instanceof B // false

设计模式

工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() { this.name = 'Person1'; }
function Animal() { this.name = 'Animal1'; }

function Factory() {}
Factory.prototype.getInstance = function(className) {
return eval('new ' + className + '()');
}

var factory = new Factory();
var obj1 = factory.getInstance('Person');
var obj2 = factory.getInstance('Animal');
console.log(obj1.name); // Person1
console.log(obj2.name); // Animal1

代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person() { }
Person.prototype.sayName = function() { console.log('michaelqin'); }
Person.prototype.sayAge = function() { console.log(30); }

function PersonProxy() {
this.person = new Person();
var that = this;
this.callMethod = function(functionName) {
console.log('before proxy:', functionName);
that.person[functionName](); // 代理
console.log('after proxy:', functionName);
}
}

var pp = new PersonProxy();
pp.callMethod('sayName'); // 代理调用Person的方法sayName()
pp.callMethod('sayAge'); // 代理调用Person的方法sayAge()

观察者,又称事件模式,例如按钮的onclick

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
function Publisher() {
this.listeners = [];
}
Publisher.prototype = {
'addListener': function(listener) {
this.listeners.push(listener);
},

'removeListener': function(listener) {
delete this.listeners[listener];
},

'notify': function(obj) {
for(var i = 0; i < this.listeners.length; i++) {
var listener = this.listeners[i];
if (typeof listener !== 'undefined') {
listener.process(obj);
}
}
}
}; // 发布者

function Subscriber() {

}
Subscriber.prototype = {
'process': function(obj) {
console.log(obj);
}
}; // 订阅者

var publisher = new Publisher();
publisher.addListener(new Subscriber());
publisher.addListener(new Subscriber());
publisher.notify({name: 'michaelqin', ageo: 30}); // 发布一个对象到所有订阅者
publisher.notify('2 subscribers will both perform process'); // 发布一个字符串到所有订阅者

原型式污染

1
2
3
4
5
6
7
let foo = {bar:1}
foo.bar // 1
foo.__proto__.bar = 233
foo.bar // 1
let zoo = {}
zoo.bar // 233
Object.prototype // { bar: 233 }

而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为233,后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了。

那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

js面试题

1
2
3
4
5
var b = 10;
(function b(){
b = 20;
console.log(b);
})();

输出:

1
2
3
4
ƒ b(){
b = 20;
console.log(b);
}

主要考察作用域和var的变量提升,如果把匿名函数中的b = 20改成var b = 20,则会输出20,上面的b其实是个函数。

1
2
3
4
5
6
7
8
9
10
function func() {
console.log(this.a);
}
var a = 2;
var o = { a: 3, func: func };
var p = { a: 4 };
o.func(); //>> 3
(p.func = o.func)(); //>> 2 ,注意:输出不是4
// 赋值表达式 p.func=o.func 的返回值是目标函数的引用,也就是 func 函数的引用
// 因此调用位置是 func() 而不是 p.func() 或者 o.func()

如何实现一个深拷贝

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
const obj = {
a: 1,
b: [1, 2, 3, 4, 5],
c: {
d: 12
}
};
obj.obj = obj;

function cloneDeep(obj, map = new WeakMap()) {
if (typeof obj === 'object') {
if (map.has(obj)) {
return map.get(obj);
}
const clone = Array.isArray(obj) ? [] : {};
// 解决循环引用
map.set(obj, clone);
for (const key of Object.keys(obj)) {
clone[key] = cloneDeep(obj[key], map);
}
return clone;
}
return obj;
}

const copy = cloneDeep(obj);
copy.obj.b.push(-1);
console.log(obj, copy);

// { a: 1, b: [ 1, 2, 3, 4, 5 ], c: { d: 12 }, obj: [Circular] } { a: 1, b: [ 1, 2, 3, 4, 5, -1 ], c: { d: 12 }, obj: [Circular] }

注意:我们常用Object.assign({},obj)来拷贝对象,但是这其实是浅拷贝,可以参考lodash.cloneDeep

ES6 let关键字的坑

不能被重复声明

1
2
3
4
function fn (a){
let a = 2;
console.log(a); //Uncaught SyntaxError: Identifier 'a' has already been declared
}

暂时性死区

1
2
3
4
5
let a = 'outside';
if(true) {
console.log(a);//Uncaught ReferenceError: Cannot access 'a' before initialization
let a = "inside";
}

js中的字符串

字符串是不可变对象,意味着我们不能单独改变某一特定索引的字符,例如:

1
2
str = '123';
str[0]='a'; // str仍然是123

解构的妙用

常用在配置的默认值初始化,例如

1
2
3
options = {host:null,port:null,...options}
// 等价于
options = Object.assign({host:null,port:null},options)