zhehua's note


  • 首页

  • 时光

  • 分类

  • 标签

  • 作品

  • 站点

  • 随想

  • 关于

JavaScript进阶之继承

发表于 2019-03-20 | 分类于 JavaScript进阶系列

导读

封装,继承和多态是面向对象的三大特性。

继承是JavaScript老生常谈的问题,也基础中的基础。最基本的就是知道JS中继承种类,优缺点并能够手写例子。下面我引用掘金Oliveryoung老兄一篇文章的思维导图,他的这篇文章写的很棒。可以点击。


继承相关的文章实在是过多,纠结了很久还是落笔,主要原因是:

  1. 继承也是JavaScript进阶系列的组成部分,同时也是自己对于知识点的梳理;
  2. 对于刚好看到这篇文章的小伙伴,可以温故知新;
  3. 把自己的理解写出来,有错误的能够指出,一起进步;

下面我以思维导图为依据,根据不同思路,给出不同的继承方式,扫盲这块知识点。

原型链继承

利用JavaScript原型链的,把父对象链到子对象的原型中一些特性,我举如下的例子,并给出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//父类对象
var Animal = function(){
this.compose = ["head","body","legs"]; //小动物的组成部分
this.statistics = {count:0}; //小动物的数量
this.category = "animal";
}
//子类对象
var Cat = function(name){
this.category = "cat";
this.name = name; //小动物名称
}

Cat.prototype = new Animal(); //原型链继承

var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);

打印结果如下:

下面是对象之间的关联关系。

tom猫的_proto_属性指向Cat.prototype,而Cat.prototype = new Animal()将Animal对象的”一切”都继承了下来。

我们接着执行如下代码:

1
2
3
4
var jemmy = new Cat("jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);

打印结果:

公有引用属性(statistics)没有问题,但是私有引用属性(compose)被污染,所以原型链继承缺点:

  1. 污染私有引用属性
  2. 无法向父对象传参

构造器继承

既然私有引用属(compose)属性不能挂载在Cat.prototype,那我们把它挂载在自雷对象上,使用call或者apply来改变context,不熟悉call和apply请猛戳这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//父类
var Animal = function (category) {
this.compose = ["head", "body", "legs"];
this.statistics = { count: 0 };
this.category = category || "animal";
}

//子类
var Cat = function (name) {
Animal.call(this,"cat");
this.name = name;
}


var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);

打印结果如下:

对象之间的关系图如下:

构造器继承解决了原型链继承的两大问题,但是又暴露了公有引用属性不能共享的问题。矫枉过正!

我们把公有引用属性使用原型链继承,私有引用属性使用构造器继承,引出组合继承(构造器+原型组合继承)

组合继承

我们将compose属性挂载在对象属性上,statistics属性挂载在原型上,结合前两种继承:

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
//父类
var Animal = function (category) {
//私有属性
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
}
Animal.prototype.statistics = {count: 0}; //公有属性放在父对象的原型上

//子类
var Cat = function (name) {
Animal.call(this,"cat");//将非共享的属性通过call加入到子对象中
this.name = name;
}

Cat.prototype = Animal.prototype; //挂载到子对象的原型中
// console.log(Cat.prototype.constructor == Animal) //true
Cat.prototype.constructor = Cat;//纠正子对象的构造函数

var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);

打印结果:


对象之间的关系图如下:

融合原型链继承和构造函数继承的优点,也是常见的继承策略。

原型继承

原型继承主要利用ES5出现的Object.create()函数,这里改写一下上述的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var Animal = function (category) {
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
this.statistics = {count: 0};
}

var animal = new Animal();
var tom = Object.create(animal);
tom.name = "Tom";
tom.category = "cat";
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);

var jemmy = Object.create(animal);
jemmy.name = "Jemmy";
jemmy.category = "cat";
jemmy.compose.push("black eyes");
jemmy.statistics.count++;
console.log(jemmy);

对象结构如下:


原型继承和原型链继承一样,只是使用Object.create()的方式进行继承。

寄生式继承

鉴于原型式继承的封装性不是很好,寄生式继承主要用于解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//父类
var Animal = function (category) {
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
this.statistics = { count: 0 };
}

//把原型式继承封装成一个create函数
function create(parent, name, category, eyes) {
let obj = Object.create(parent);
obj.name = name;
obj.category = category;
obj.compose.push(eyes);
obj.statistics.count++;
return obj;
}

var animal = new Animal();
var tom = create(animal, "Tom", "cat", "blue eyes");
console.log(tom);
var jemmy = create(animal, "Jemmy", "cat", "black eyes");
console.log(jemmy);

这种方式比原型式继承封装性更好。但是缺点还是没解决的。在寄生式基础上,结合构造器继承,就是寄生组合式继承。

寄生组合式继承

寄生组合式继承,更多了还是利用组合式继承的思想。

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
//父类
var Animal = function (category) {
//私有属性
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
}

Animal.prototype = {
//公有属性,还是放在原型上
statistics : { count: 0 }
}

var Cat = function(name){
//把私有属性通过call继承过来
Animal.call(this,"cat");
this.name = name;
}

function proto(Son,Parent) {
//其实就是把组合继承的原型部分封装了一下
Son.prototype = Object.create(Parent.prototype);
Son.prototype.constructor = Son;
}
proto(Cat,Animal);

var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);

打印结果和组合式继承一致:

总结一下,JavaScript的继承的思想主要由下面两条构成:

  1. 将私有属性通过call/apply在子对象构造函数中调用,直接继承
  2. 将公有属性通过原型链继承

组合式继承和寄生组合式继承只是实现方式的不同,思想是一致的。

ES6

class和extends的出现,使继承变得简单!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal{
constructor(category){
this.compose = ["head", "body", "legs"];
this.category = category || "animal";
}
}

Animal.prototype.statistics = { count: 0 }

class Cat extends Animal{
constructor(name){
super("cat");
this.name = name;
}
}
var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);

var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);

打印结果如下:

我们使用babel转成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
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
"use strict";

function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf :
function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}

function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
//子类的原型指向一个以(subClass为构造函数,superClass.prototype中的对象属性)的对象
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true } });
if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf ||
function _setPrototypeOf(o, p)
{ o.__proto__ = p; return o; };
return _setPrototypeOf(o, p);
}

function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return right[Symbol.hasInstance](left); } else { return left instanceof right; } }

function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Animal = function Animal(category) {
_classCallCheck(this, Animal);

this.compose = ["head", "body", "legs"];
this.category = category || "animal";
};

Animal.prototype.statistics = {
count: 0
};

var Cat =
/*#__PURE__*/
function (_Animal) {
_inherits(Cat, _Animal);

function Cat(name) {
var _this;

_classCallCheck(this, Cat);

_this = _possibleConstructorReturn(this, _getPrototypeOf(Cat).call(this, "cat"));
_this.name = name;
return _this;
}

return Cat;
}(Animal);

大家可以仔细看看这段代码,写的挺有意思的,看懂了基本上也就理解继承了。在这里
_inherits(Cat, _Animal)跟寄生组合式继承中的proto(Cat,Animal)一样,_getPrototypeOf(Cat).call(this, "cat")和Animal.call(this,"cat");也一样,值得一提的是ES6中的super(xxx)就是将父类的构造函数使用call进行传参。

参考

  1. JavaScript深入之继承的多种方式和优缺点
  2. 一文看懂 JS 继承

跟underscore一起学函数去重

发表于 2019-03-10 | 分类于 Underscore源码系列

引子

数组去重是一个老生常谈的话题,在面试中也经常会被问道。对于去重,有两种主流思想:

  1. 先排序,线性遍历后去重,时间复杂度O(n*log2n);
  2. 使用哈希,空间换时间,时间复杂度O(n);

上一篇文章,我分析了underscore的函数是如何组织的,我们能够依照这种方法书写自己的函数库,这篇文章,来看看关于函数去重underscore是如何做的?

Underscore的去重

功能介绍

underscore的去重是指数组(Arrays)中uniq函数,其API如下:

uniq _.uniq(array, [isSorted], [iteratee]) 别名: unique
说明:返回 array去重后的副本, 使用 === 做相等测试. 如果您确定 array 已经排序, 那么给 isSorted 参数传递 true值, 此函数将运行的更快的算法. 如果要处理对象元素, 传参 iterator 来获取要对比的属性.

上述API主要想说明几点:

  1. 返回数组副本,不影响原数组
  2. 相等的标准是a===b,表明不仅要值相等,类型也需要相等
  3. 如果数组是排序的,去重运算效率更高
  4. uniq也可以比较对象,前提是需要指定比较的对象属性

我们简单使用以下_.uniq(array, [isSorted], [iteratee]),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log(_.uniq([1,4,2,2,3,3]));
console.log(_.uniq([1,2,2,2,3,3],true));
console.log(_.uniq([{
name:1,
gender:"male"
},{
name:2,
gender:"female"
},{
name:2,
gender:"male"
},{
name:4,
gender:"male"
}],true,"gender"));

结果如下:

去重思想及实现

underscore去重的核心思想:

新建结果集数组res,遍历待去重数组,将每个遍历值在res数组中遍历检查,将不存在当前res中的遍历值压入res中,最后输出res数组。

1
2
3
4
5
6
7
8
9
10
function uniq(array){
var res = [];
array.forEach(function(element) {
if(res.indexOf(element)<0){
res.push(element);
}
}, this);
return res;
}
console.log(uniq([1,4,2,2,3,3])); //[1,4,2,3]

其中如果数组是排序的,去重运算效率更高,因为排序能够将相同的数排列在一起,方便前后比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function uniq(array, isSorted) {
var res = [];
var seen = null;

array.forEach(function (element,index) {
if (isSorted) {
//当数组有序
if(!index || seen !== element) res.push(element);
seen = element;
} else {
if (res.indexOf(element) < 0) {
res.push(element);
}
}
}, this);
return res;
}
console.log(uniq([1,2,"2",3,3,3,5],true)); //(5) [1, 2, "2", 3, 5]

对于对象的去重,我们知道{}==={}为false,所以使用===比较对象在实际场景中没有意义。
在这里我举个实际场景的例子:

我要在小组中选择一名男生(male)和一名女生(female),小组组员情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var array = [{
name:"Tom",
gender:"female"
},{
name:"Lucy",
gender:"female"
},{
name:"Edward",
gender:"male"
},{
name:"Molly",
gender:"female"
}]

我们修改上面的uniq:

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 uniq(array, isSorted, iteratee) {
var res = [];
var seen = [];
array.forEach(function (element, index) {
if (iteratee) {
//判断iteratee是否存在,存在的话,取出真正要比较的属性
var computed = element[iteratee];
if (seen.indexOf(computed) < 0) {
seen.push(computed);
res.push(element);
}
} else if (isSorted) {
//当数组有序
if (!index || seen !== element) res.push(element);
seen = element;
} else {
if (res.indexOf(element) < 0) {
res.push(element);
}
}
}, this);
return res;
}
console.log(uniq([{
name:"Tom",
gender:"female"
},{
name:"Lucy",
gender:"female"
},{
name:"Edward",
gender:"male"
},{
name:"Molly",
gender:"female"
}],true,"gender"));

结果如下:

underscore的uniq的实现,基本上使用的上述思想。在附录中我附上了源码和一些注释。

关于去重的思考

上述我分析了underscore的uniq函数实现,在这之前我也看过诸如《JavaScript去重的N种方法》…之类的文章,underscore中的uniq函数实现方法并不是最优解,至少从时间复杂度来讲不是最优。
那么为什么underscore不用Set对象来解决去重问题,使用indexof查找的时间复杂度是O(n),而hash查询是O(1)。
我个人认为Set是ES6中引的对象,underscore是为了考虑兼容性问题。
那为什么不用obj作为Set的替代方案呢?
这里我猜是underscore的设计者只想用自己内部实现的_.indexOf函数。此处是我的猜测,大家如果有想法,欢迎大家留言!
下面我附上ES6的实现(大家最熟悉的):

1
2
var a = [1,1,2,3,4,4];
var res = [...new Set(a)];

再附上obj的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function uniq(array,iteratee){
var res = [];
var obj = {};
array.forEach(function(element) {
var computed = element;
if(iteratee) computed = element[iteratee];
if(!obj.hasOwnProperty(computed))
obj[(typeof computed)+"_"+JSON.stringify(computed)] = element;
}, this);
for(var p in obj){
res.push(obj[p]);
}
return res;
}
uniq([1,"1",2,3,4,4]);// (5) [1, "1", 2, 3, 4]

附录

underscore的uniq函数源码及注释:

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
  _.uniq = _.unique = function(array, isSorted, iteratee, context) {
if (array == null) return [];
if (!_.isBoolean(isSorted)) {
//如果没有排序
context = iteratee;
iteratee = isSorted;
isSorted = false;
}
/**
** 此处_.iteratee
** function (key){
* return function(obj){
* return obj[key];
* }
** }
** key就是这里的iteratee(对象的属性),这里使用了闭包
**/
if (iteratee != null) iteratee = _.iteratee(iteratee, context);
var result = [];//返回去重后的数组(副本)
var seen = [];
for (var i = 0, length = array.length; i < length; i++) {
var value = array[i];//当前比较值
if (isSorted) {
//如果i=0时,或者seen(上一个值)不等于当前值,放入去重数组中
if (!i || seen !== value) result.push(value);
seen = value;//保存当前值,用于下一次比较
} else if (iteratee) {
var computed = iteratee(value, i, array);
if (_.indexOf(seen, computed) < 0) {
seen.push(computed);
result.push(value);
}
} else if (_.indexOf(result, value) < 0) {
result.push(value);
}
}
return result;
};

跟underscore一起学如何写函数库

发表于 2019-03-07 | 分类于 underscore源码系列

目的

Underscore 是一个 JavaScript 工具库,它提供了一整套函数式编程的实用功能,但是没有扩展任何 JavaScript 内置对象。

本文主要用于梳理和研究underscore内部是如何组织和处理函数的。

通过这篇文章,我们可以:

了解underscore在函数组织方面的巧妙构思;

为自己书写函数库提供一定思路;

我们开始!

自己写个函数库

前端的小伙伴一定不会对jQuery陌生,经常使用$.xxxx的形式进行进行调用,underscore也使用_.xxxx,如果自己在ES5语法中写过自定义模块的话,就可以撸出下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
(function(){
//定义
var root = this;
var _ = {};
_.first = function(arr,n=0){
if(n==0) return arr[0];
return arr.slice(0,n);
}
root._ = _;
})();
console.log(this);

在Chrome浏览器中打开之后,打印出如下结果:

image-20190305235718888

我们看到在全局对象下有一个_属性,属性下面挂载了自定义函数,我们不妨使用_.first(xxxx)在全局环境下直接调用。

1
2
3
console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));

输出结果如下:

image-20190306000334727

没问题,我们的函数库制作完成了,我们一般直接这么用,也不会有太大问题。

underscore是怎么做的?

underscore正是基于上述代码的完善,那么underscore是如何接着往下做的呢?容我娓娓道来!

对兼容性的考虑

1
2
3
4
5
6
7
// Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self`
// instead of `window` for `WebWorker` support.
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};

上面是underscore1.9.1IIFE函数中的源码,对应于我们上面自己写的var root = this;。

在源码中作者也解释了:创建root对象,并且给root赋值:

浏览器端:window也可以是window.self或者直接self

服务端(node):global

WebWorker:self

虚拟机:this

underscore充分考虑了兼容性。

支持两种不同风格的函数调用

在underscore中我们可以使用两种方式调用函数:

  1. 函数式的调用:console.log(_.first([1,2,3,4]));
  2. 对象式调用:console.log(_([1,2,3,4])).first();

在underscore中,它们返回的结果都是相同的。

第一种方式没有问题,难点就是第二种方式的调用。

对象式调用的实现

解决这个问题要达到两个目的:

  1. _是一个函数,并且调用返回一个对象;
  2. 这个对象依然能够调用挂载在_对象上声明的方法。

我们来看看underscore对于_的实现:

1
2
3
4
5
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

相關圖片

不怕,我们不妨调用_([1,2,3,4]))看看他是怎么执行的!

第一步:if (obj instanceof _) return obj;传入的对象及其原型链上有_类型的对象,则返回自身。我们这里的[1,2,3,4]显然不是,跳过。

第二步:if (!(this instanceof _)) return new _(obj);,如果当前的this对象及其原型链上没有_类型的对象,那么执行new操作。调用_([1,2,3,4]))时,this为window,那么(this instanceof _)为false,所以我们执行new _([1,2,3,4])。

第三步:执行new _([1,2,3,4]),继续调用_函数,这时

obj为[1,2,3,4]

this为一个新对象,并且这个对象的__proto__指向_.prototype(对于new对象执行有疑问,请猛戳此处)

此时

(obj instanceof _)为false

(this instanceof _)为true

所以此处会执行this._wrapped = obj;,在新对象中,添加_wrapped属性,将[1,2,3,4]挂载进去。

综合上述函数实现的效果就是:

_([1,2,3,4]))<=====>new _([1,2,3,4])

然后执行如下构造函数:

1
2
3
var _ = function(obj){
this._wrapped = obj
}

最后得到的对象为:

image-20190306201849178

image-20190306235445836

我们执行如下代码:

1
2
3
console.log(_([1,2,3,4]));
console.log(_.prototype);
console.log(_([1,2,3,4]).__proto__ == _.prototype);

看一下打印的信息:

image-20190306214133549

这表明通过_(obj)构建出来的对象确实具有两个特征:

  1. 下面挂载了我们传入的对象或数组
  2. 对象的_proto_属性指向_的prototype

到此我们已经完成了第一个问题。

「我正是个天才 表情包」的圖片搜尋結果

接着解决第二个问题:

这个对象依然能够调用挂载在_对象上声明的方法

我们先来执行如下代码:

1
_([1,2,3,4]).first();

此时JavaScript执行器会先去找_([1,2,3,4])返回的对象上是否有first属性,如果没有就会顺着对象的原型链上去找first属性,直到找到并执行它。

我们发现_([1,2,3,4])返回的对象属性和原型链上都没有first!

image-20190307000429320

那我们自己先在_.prototype上面加一个上去试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function(){
//定义
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};

var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

_.first = function(arr,n=0){
if(n==0) return arr[0];
return arr.slice(0,n);
}
_.prototype.first = function(arr,n=0){
if(n==0) return arr[0];
return arr.slice(0,n);
}
root._ = _;
})();

我们在执行打印一下:

1
console.log(_([1,2,3,4]));

效果如下:

image-20190306214554433

原型链上找到了first函数,我们可以调用first函数了。如下:

1
console.log(_([1,2,3,4]).first());

可惜报错了:

image-20190306214848922

于是调试一下:
image-20190306214932983

我们发现arr是undefined,但是我们希望arr是[1,2,3,4]。

「不慌 表情包」的圖片搜尋結果

我们马上改一下_.prototype.first的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function(){

var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};

var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

_.first = function(arr,n=0){
if(n==0) return arr[0];
return arr.slice(0,n);
}
_.prototype.first = function(arr,n=0){
arr = this._wrapped;
if(n==0) return arr[0];
return arr.slice(0,n);
}
root._ = _;
})();

我们在执行一下代码:

1
console.log(_([1,2,3,4]).first());

效果如下:

image-20190306215555025

我们的效果似乎已经达到了!

「赞 表情包」的圖片搜尋結果

现在我们执行下面的代码:

1
console.log(_([1,2,3,4]).first(2));

调试一下:

image-20190306215729756

凉凉了。

「凉凉 表情包」的圖片搜尋結果

其实我们希望的是:

将[1,2,3,4]和2以arguments的形式传入first函数

我们再来改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//_.prototype.first = function(arr,n=0){
// arr = this._wrapped;
// if(n==0) return arr[0];
// return arr.slice(0,n);
//}
_.prototype.first=function(){
/**
* 搜集待传入的参数
*/
var that = this._wrapped;
var args = [that].concat(Array.from(arguments));
console.log(args);
}

我们再执行下面代码:

1
_([1,2,3,4]).first(2);

看一下打印的效果:

image-20190306220704637

参数都已经拿到了。

我们调用函数一下first函数,我们继续改代码:

1
2
3
4
5
6
7
8
9
10
11
_.prototype.first=function(){
/**
* 搜集待传入的参数
*/
var that = this._wrapped;
var args = [that].concat(Array.from(arguments));
/**
* 调用在_属性上的first函数
*/
return _.first(...args);
}

这样一来_.prototype上面的函数的实际实现都省掉了,相当于做一层代理,调用一下。

一举两得!

执行一下最初我们的代码:

1
2
3
console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));

image-20190306221231484

现在好像我们所有的问题都解决了。

「赞 表情包」的圖片搜尋結果

但是似乎每声明一个函数都得在原型链上也声明一个相同名字的函数。形如下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_.a = function(args){
//a的实现
}
_.prototype.a = function(){
//调用_.a(args)
}
_.b = function(args){
//b的实现
}
_.prototype.b = function(){
//调用_.b(args)
}
_.c = function(args){
//c的实现
}
_.prototype.c = function(){
//调用_.c(args)
}
.
.
.
1000个函数之后...

会不会觉得太恐怖了!

「害怕 表情包」的圖片搜尋結果

我们能不能改成如下这样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_.a = function(args){
//a的实现
}
_.b = function(args){
//b的实现
}
_.c = function(args){
//c的实现
}
1000个函数之后...
_.mixin = function(){
//将_属性中声明的函数都挂载在_prototype上面
}
_.mixin(_);

上面这么做好处大大的:

我们可以专注于函数库的实现,不用机械式的复写prototype上的函数。

underscore也正是这么做的!

我们看看mixin函数在underscore中的源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Add your own custom functions to the Underscore object.
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply(_, args));
};
});
return _;
};

// Add all of the Underscore functions to the wrapper object.
_.mixin(_);

有了上面的铺垫,这个代码一点都不难看懂,首先调用_.each函数,形式如下:

1
2
3
_.each(arrs, function(item) {
//遍历arrs数组中的每一个元素
}

我们一想就明白,我们在_对象属性上实现了自己定义的函数,那么现在要把它们挂载到_的prototype属性上面,当然先要遍历它们了。

所以我们可以猜到_.functions(obj)肯定返回的是一个数组,而且这个数组肯定是存储_对象属性上面关于我们实现的各个函数的信息。

我们看一下_.function(obj)的实现:

1
2
3
4
5
6
7
8
9
10
11
_.functions = _.methods = function(obj) {
var names = [];
/**
** 遍历对象中的属性
**/
for (var key in obj) {
//如果属性值是函数,那么存入names数组中
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

确实是这样的!

「拉菲 表情包」的圖片搜尋結果

我们把上述实现的代码整合起来:

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
(function(){
/**
* 保证兼容性
*/
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};

/**
* 在调用_(obj)时,让其执行new _(obj),并将obj挂载在_wrapped属性之下
*/
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

//自己实现的first函数
_.first = function(arr,n=0){
if(n==0) return arr[0];
return arr.slice(0,n);
}

//判断是否是函数
_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};

//遍历生成数组存储_对象的函数值属性
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

//自己实现的遍历数组的函数
_.each = function(arrs,callback){
for(let i=0;i<arrs.length;i++){
callback(arrs[i]);
}
}

var ArrayProto = Array.prototype;
var push = ArrayProto.push;

//underscore实现的mixin函数
_.mixin = function(obj) {
console.log(_.functions(obj)); //我们打印一下_.functions(_)到底存储了什么?
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return func.apply(_, args);
};
});
return _;
};

//执行minxin函数
_.mixin(_);
root._ = _;
})();

我们看一下_.functions(obj)返回的打印信息:

image-20190306224747300

确实是_中自定义函数的属性值。

我们再来分析一下each中callback遍历各个属性的实现逻辑。

1
2
3
4
5
6
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return func.apply(_, args);
};

第一句:func变量存储每个自定义函数

第二句: _.prototype[name]=function();在_.prototype上面也声明相同属性的函数

第三句:args变量存储_wrapped下面挂载的值

第四句:跟var args = [that].concat(Array.from(arguments));作用相似,将两边的参数结合起来

第五句:执行func变量指向的函数,执行apply函数,将上下文对象_和待传入的参数`args``传入即可。

我们再执行以下代码:

1
2
3
console.log(_.first([1,2,3,4]));
console.log(_.first([1,2,3,4],1));
console.log(_.first([1,2,3,4],3));

结果如下:

image-20190306230712917

Perfect!

这个函数在我们的浏览器中使用已经没有问题。

但是在Node中呢?所以下面引出新的问题。

再回归兼容性问题

我们知道在Node中,我们是这样的:

1
2
3
4
5
6
//a.js
let a = 1;
module.exports = a;
//index.js
let b = require('./a.js');
console.log(b) //打印1

那么:

1
2
let _ = require('./underscore.js')
_([1,2,3,4]).first(2);

我们也希望上述的代码能够在Node中执行。

所以root._ = _是不够的。

underscore是怎么做的呢?

如下:

1
2
3
4
5
6
7
8
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}

我们看到当全局属性exports不存在或者不是DOM节点时,说明它在浏览器中,所以:

root._ = _;

如果exports存在,那么就是在Node环境下,我们再来进行判断:

如果module存在,并且不是DOM节点,并且module.exports也存在的话,那么执行:

exports = module.exports = _;

在统一执行:

exports._ = _;

附录

下面是最后整合的阉割版underscore代码:

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
67
68
69
70
71
72
73
(function(){
/**
* 保证兼容性
*/
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};

/**
* 在调用_(obj)时,让其执行new _(obj),并将obj挂载在_wrapped属性之下
*/
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

//自己实现的first函数
_.first = function(arr,n=0){
if(n==0) return arr[0];
return arr.slice(0,n);
}

//判断是否是函数
_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};

//遍历生成数组存储_对象的函数值属性
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

//自己实现的遍历数组的函数
_.each = function(arrs,callback){
for(let i=0;i<arrs.length;i++){
callback(arrs[i]);
}
}

var ArrayProto = Array.prototype;
var push = ArrayProto.push;

//underscore实现的mixin函数
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return func.apply(_, args);
};
});
return _;
};


//执行minxin函数
_.mixin(_);
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
})();

欢迎各位大佬拍砖!同时您的点赞是我写作的动力~谢谢。

【译】CSS对象模型(CSSOM)指南

发表于 2019-03-05 | 分类于 CSS

作者:Louis Lazaris

原文链接:https://css-tricks.com/an-introduction-and-guide-to-the-css-object-model-cssom/

如果您已经写了一段时间的JavaScript,那么可以肯定您使用过用于处理文档对象模型(DOM)的脚本。DOM脚本利用网页提供的一组API(或接口)来操作和处理页面上的元素。

那么,您应该还熟悉另一个对象模型:CSS对象模型(CSSOM),可能你已经在无意识的时候使用过它。

在这篇文章中,我将介绍CSSOM的许多重要的功能,从最常见的开始,慢慢转向一些大家不清楚但很实用的小功能。

#什么是CSSOM?

根据MDN:

CSS对象模型是JavaScript处理CSS的API。它很像DOM,只是面向CSS而不是HTML。它允许用户动态地读取和修改CSS样式。

MDN的信息基于 官方的W3C CSSOM规范. 用W3C文档来熟悉CSSOM的是一种低效的方式,也不适用于那些正在寻找一些实际编码示例的人。

使用MDN显然会好得多,虽然在某些方面仍然很缺乏。因此,对于这篇文章,我试图尽力的去创建有用的代码示例来演示,这样您就可以通过实际代码解除疑惑。

就像上面所说,文章从大多数前端开发人员熟悉的内容开始。里面有一些公有属性常常和DOM脚本混在一起,但是CSSOM的API还是占了很大一部分。

#使用element.style获取内联样式

使用JavaScript操作和获取CSS属性和值的最基本方法是通过style对象或属性,这个对象和属性往往对于所有的HTML元素都适用。这里有一个例子:

1
document.body.style.background = 'lightblue';

大多数人之前可能已经看过或用过这种语法。我可以使用element.style.propertyName.的格式为页面上的任何对象添加或更改CSS 。

在上述例子中,我正在将background属性的值更改为lightblue。当然,background是“短属性”(只有一个单词组成的属性)。如果我只想更改background-color属性怎么办?对于任何带连字符的属性,只需将属性名称转换为驼峰法的形式,如下:

1
document.body.style.backgroundColor = 'lightblue';

在大多数情况下,“短属性”以小写单词的形式表示和访问,而“连字符属性”用驼峰法表示。但是有一个例外是使用float属性。因为float是JavaScript中的保留字,所以您需要使用cssFloat(如果您支持IE8及更早版本,请使用styleFloat)。这类似于HTML中的for属性,我们使用htmlFor 来表示。

下面是一个使用style属性来允许用户更改当前页面背景颜色的例子:

我们使用JavaScript定义CSS属性和值虽然简单方法,但是使用style属性有一个很大的前提:只适用于元素的内联样式。

当您使用该style属性读取CSS 时,这一点变得清晰:

1
2
3
document.body.style.backgroundColor = 'lightblue';
console.log(document.body.style.backgroundColor);
// "lightblue"

在上面的例子中,我在<body>元素上定义了一个内联样式,然后我将相同的样式输出到控制台。但是如果我尝试去读取style的另一个属性的值时,它将不返回任何内容 - 除非是我之前在CSS或JavaScript中定义的内联样式。例如:

1
2
console.log(document.body.style.color);
// Returns nothing if inline style doesn't exist

即使我使用外部样式表的color 属性去修饰<body> 元素,也同样获取不到值,如下所示:

通过JavaScript使用element.style的方式去改变元素的属性是最简单和通用的方式,但是你也看到这里很明显有很大的限制,让我们继续来看一下JavaScript还有那些更好用的方式去读取和修改我们的样式。

#获取Computed样式

您可以使用window.getComputedStyle() 方法来读取元素上的任何CSS属性:

1
2
window.getComputedStyle(document.body).background;
// "rgba(0, 0, 0, 0) none repeat scroll 0% 0% / auto padding-box border-box"

哇!真实一个有意思的结果。在某种程度上,window.getComputedStyle()是style的孪生兄弟。如果直接使用style属性给你提供的元素实际样式信息太少,那么使用window.getComputedStyle()会给您一些启发。

在上述示例中,<body>元素的background属性是使用background: papayawhip;定义。但getComputedStyle()方法返回background属性中的所有值,未在CSS中明确定义的那些将返回这个属性的初始(或默认)值。

这意味着,对于任何属性,即使CSS中没有定义它们,使用window.getComputedStyle()仍可以返回所有初始值:

类似地,比如width和height这样的属性,不管这些值是否在CSS中的任何位置定义,它将显示元素对应属性的计算值,如下面的示例所示:

尝试调整上面示例中的父元素大小以查看结果。这类似于读取window.innerWidth值,只是在这边是去获取指定元素上指定属性的CSS值,而不仅仅是一般的窗口或视口。

这里我们使用window.getComputedStyle()方法中的几种方式来访问。我已经演示了其中的一种方法,它使用点符号+驼峰式属性名称的方式添加到方法的末尾。您可以在以下代码中看到三种不同的方法:

1
2
3
4
5
6
7
8
// dot notation, same as above
window.getComputedStyle(el).backgroundColor;

// square bracket notation
window.getComputedStyle(el)['background-color'];

// using getPropertyValue()
window.getComputedStyle(el).getPropertyValue('background-color');

上面第一种方法的形式和先前的那个例子一样。

第二种方式使用中括号将CSS属性用单引号包裹起来,这种方式不推荐,代码编译器会告警。

第三种使用getPropertyValue() 的方式。

在第一个例子中,我们使用骆驼法(在这个例子中,float和cssFloat属性都可以工作),而接下来的两个方法使用相同的语法形式,使用CSS中的属性(连字符,通常被称为“烤肉箱”)。

下面的示例与前一个相同,但这次使用getPropertyValue()访问两个元素的宽度:


See the Pen
pGeQOy
by 宣浙华 (@zhehuaxuan)
on CodePen.

#获取伪元素的计算样式

一个鲜为人知的事情是window.getComputedStyle()能够获取伪元素的样式信息。你可以看到 window.getComputedStyle() 方法像如下形式的声明:

1
window.getComputedStyle(document.body, null).width;

请注意第二个参数, null, Firefox的版本4之前的需要第二个参数,这就是为什么你经常在老代码中看到它(目前所有的浏览器都不需要这个参数)。

第二个参数可选,允许我们去获得当前元素的伪元素的属性,如下所示:

1
2
3
4
5
.box::before {
content: 'Example';
display: block;
width: 50px;
}

在这里,我们给.box 元素添加了一个::before 的伪元素,下面的JavaScript代码用于计算伪元素的属性值:

1
2
3
let box = document.querySelector('.box');
window.getComputedStyle(box, '::before').width;
// "50px"

您也可以为其他伪元素执行此操作::first-line,如以下代码和演示:

1
2
let p = document.querySelector('.box p');
window.getComputedStyle(p, '::first-line').color;

And here’s another example using the ::placeholder pseudo-element, which apples to <input>elements:

这里有另外一个例子获取input标签中的::placeholder伪元素的样式信息:

1
2
let input = document.querySelector('input');
window.getComputedStyle(input, '::placeholder').color

以上工作在最新的Firefox中,但不适用于Chrome或Edge(我已经为Chrome 提交了错误报告)。

还应该注意的是,尝试访问不存在(但有效)的伪元素的样式时,浏览器会有不同的结果(::banana伪元素)。您可以使用以下演示在各种浏览器中查看效果:

作为本节的一个小点,有一个名为Firefox的方法getDefaultComputedStyle(),它不是规范的一部分,可能永远不会。

#CSS样式声明API

上面我向您展示如何通过style对象或使用getComputedStyle()函数访问属性,在这两种情况下都暴露了CSSStyleDeclaration接口。

换句话说,以下两行都将返回CSS样式文档body元素上的对象:

1
2
document.body.style;
window.getComputedStyle(document.body);

下面是这两个对象返回的快照:

The CSSStyleDeclaration API in the DevTools console

getComputedStyle()函数返回的值是只读的,使用element.style可以获取和设置属性,但是像先前提到的,这些只会影响document的内联样式。

#setProperty(), getPropertyValue(), 和item()

一旦您以上述方式之一暴露了CSS样式声明对象,您就可以访问许多有用的方法来读取或操作这些值。同样,getComputedStyle()返回值是只读的,但是当通过style属性使用时,可用于获取和设置属性值。

请考虑以下代码和演示:

1
2
3
4
5
6
let box = document.querySelector('.box');

box.style.setProperty('color', 'orange');
box.style.setProperty('font-family', 'Georgia, serif');
op.innerHTML = box.style.getPropertyValue('color');
op2.innerHTML = `${box.style.item(0)}, ${box.style.item(1)}`;

在这个例子中,我使用了三种不同的style对象方法:

  • setProperty()方法。这需要两个参数,每个参数都是一个字符串:属性(以常规CSS表示法)和属性值
  • getPropertyValue()方法。这需要一个参数,你想要获得的属性。和前面提到的getComputedStyle()方法一样,返回 CSSStyleDeclaration 对象。
  • item()方法。这需要使用一个参数,这需要一个参数,它是一个正整数,表示您要访问的属性的索引。返回值是该索引处的属性名称。

注意,在我上面的简单示例中,只有两个样式添加到元素的内联CSS中。这意味着如果我要访问item(2),返回值将是一个空字符串。如果我getPropertyValue()以前访问未在该元素的内联样式中设置的属性,也会得到相同的结果。

# 使用removeProperty()

除了上面提到的三种方法之外,还有另外两种方法暴露CSS样式声明对象。在下面的代码和示例中,我将使用removeProperty()方法:

1
2
3
4
5
box.style.setProperty('font-size', '1.5em');
box.style.item(0) // "font-size"

document.body.style.removeProperty('font-size');
document.body.style.item(0); // ""

In this case, after I set font-size using setProperty(), I log the property name to ensure it’s there. The demo then includes a button that, when clicked, will remove the property using removeProperty().

在这种例子中,我们使用setProperty()设置font-size之后,我记录属性名称,然后使用一个按钮,单击该按钮使用removeProperty()删除该属性。

#获取和设置属性的优先级

最后,这是我在写这篇文章的时候发现了一个有趣的特性:使用getPropertyPriority(),看下面CodePen演示的示例:

1
2
3
4
5
box.style.setProperty('font-family', 'Georgia, serif', 'important');
box.style.setProperty('font-size', '1.5em');

box.style.getPropertyPriority('font-family'); // important
op2.innerHTML = box.style.getPropertyPriority('font-size'); // ""

在该代码的第一行,您可以看到我使用setProperty()方法,就像我之前一样。但是,我们在这里包含了第三个参数。第三个参数是一个可选字符串,用于定义是否希望该属性附加!important关键字。

在我为属性设置!important之后,我们使用getPropertyPriority()方法检查该属性的优先级。如果您觉得该属性不重要,可以省略第三个参数,使用关键字undefined,或将第三个参数包含为空字符串。

我应该在这里强调,这些方法可以与已经直接放在HTML元素style属性上的任何内联样式一起使用。

所以,如果我有如下HTML:

1
<div class="box" style="border: solid 1px red !important;">

我可以使用本节中讨论的任何方法来读取或修改该样式。这里应该注意的是,由于我使用了这种内联样式的简写属性并将其设置为!important,因此我们使用getPropertyPriority()就可以把长属性的important返回出来,请参阅下面的代码和演示:

1
2
3
4
5
6
// These all return "important"
box.style.getPropertyPriority('border'));
box.style.getPropertyPriority('border-top-width'));
box.style.getPropertyPriority('border-bottom-width'));
box.style.getPropertyPriority('border-color'));
box.style.getPropertyPriority('border-style'));

在这个例子中,即使我只在border属性中显式设置了style属性的important,所有关联border的长属性也都返回important。

# CSS样式表API

到目前为止,我所考虑的方法都涉及内联样式(通常不那么有用)和计算样式(这些方式很有用,但通常过于具体)。

还有一个更有用的API,允许您获取具有可读写性的样式表,而不仅仅是内联样式,这就是CSS样式表。从文档样式表访问文档属性的最简单方法是使用styleSheets属性。这就是CSS样式表API。

例如,下面的使用该length属性来查看当前文档具有多少个样式表:

1
document.styleSheets.length; // 1

我可以使用从零开始的索引来引用任何文档的样式表:

1
document.styleSheets[0];

如果我们用console打印stylesheet ,我们将会看到下面的这些方法和属性:

The CSSStyleSheet Interface in the DevTools Console

这里最有用的属性是cssRules。此属性提供样式表中包含的所有CSS规则(包括声明块,规则,媒体规则等)。在下面,我将详细介绍如何利用此API来操作和读取外部样式表中的样式。

#使用样式表对象

为了简单起见,让我们使用外部样式表,其中只包含少量规则。这里请允许我演示如何使用CSSOM访问样式表的不同部分,和DOM脚本的方式类似。

这是我使用下面的样式表:

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
* {
box-sizing: border-box;
}

body {
font-family: Helvetica, Arial, sans-serif;
font-size: 2em;
line-height: 1.4;
}

main {
width: 1024px;
margin: 0 auto !important;
}

.component {
float: right;
border-left: solid 1px #444;
margin-left: 20px;
}

@media (max-width: 800px) {
body {
line-height: 1.2;
}

.component {
float: none;
margin: 0;
}
}

a:hover {
color: lightgreen;
}

@keyframes exampleAnimation {
from {
color: blue;
}

20% {
color: orange;
}

to {
color: green;
}
}

code {
color: firebrick;
}

我可以在这个例子中尝试很多事情,首先,我将循环遍历样式表中的所有样式规则,并记录每个样式的选择器的值:

1
2
3
4
5
6
7
8
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('p');

for (i of myRules) {
if (i.type === 1) {
p.innerHTML += `<code>${i.selectorText}</code><br>`;
}
}

在上面的示例中需要注意的几件事情。首先,我定义记录外部样式表的cssRules对象的引用。然后我遍历该对象中的所有规则,检查每个规则的类型。

在这种情况下,我想要类型的规则1,它代表STYLE_RULE常量。其他常量包括IMPORT_RULE(3),MEDIA_RULE(4),KEYFRAMES_RULE(7)等。您可以在此MDN文章中查看这些常量的完整表。

当我确认规则是样式规则时,我打印每个样式规则的selectorText属性。展示如下结果:

1
2
3
4
5
6
*
body
main
.component
a:hover
code

selectorText属性表示选择器样式规则的名称。这是一个可写属性,所以如果我用for使用以下代码更改原始循环内特定规则的选择器:

1
2
3
if (i.selectorText === 'a:hover') {
i.selectorText = 'a:hover, a:active';
}

在这个例子中,我查询一个选择器(链接),把 :hover 属性查询出来,应用到 :active 中。而且,我可以使用字符串方法甚至是正则表达式来查找所有实例的:hover,然后从那里做一些事情。但这个例子已经足以证明它的工作原理。

#使用CSSOM访问@media规则

您会注意到我的样式表还包括媒体查询规则:@keyframes at-rule。当我搜索样式规则(类型1)时,这两个都被跳过了。我们现在找到所有@media规则:

1
2
3
4
5
6
7
8
9
10
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 4) {
for (j of i.cssRules) {
p.innerHTML += `<code>${j.selectorText}</code><br>`;
}
}
}

基于给定的样式表,上面将产生:

1
2
body
.component

正如您所看到的,在我遍历所有规则以查看是否存在@media规则(类型4)之后,我遍历cssRules的每个媒体规则对象(在这种情况下,只有一个)并记录在那个媒体规则里面的每个规则的选择器文本。

因此,@media规则上公开的接口类似于样式表上公开的接口。@media中的规则还包含一个conditionText属性,如以下代码段和演示中所示:

1
2
3
4
5
6
7
8
9
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 4) {
p.innerHTML += `<code>${i.conditionText}</code><br>`;
// (max-width: 800px)
}
}

此代码循环遍历所有媒体查询规则,并记录确定该规则何时适用的文本(即条件)。还有一个mediaText返回相同值的属性。根据规范,您可以获得或设置其中任何一个。

#使用CSSOM访问@keyframes规则

上述我已经演示了如何读取@media规则中的信息,现在让我们考虑如何访问@keyframes规则。我们从下面的代码开始:

1
2
3
4
5
6
7
8
9
10
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 7) {
for (j of i.cssRules) {
p.innerHTML += `<code>${j.keyText}</code><br>`;
}
}
}

在这个例子中,我正在查询类型为7(i.e. @keyframes规则)的规则。当找到一个规则时,遍历所有规则cssRules并记录每个规则的keyText属性。这种情况下的日志将是:

1
2
3
"0%"
"20%"
"100%"

您会注意到我的原始CSS使用from并to作为第一个和最后一个关键帧,但是keyText属性计算这些0%和100%。keyText也可以设置值。在我的示例样式表中,我可以像这样硬编码:

1
2
3
4
5
6
7
8
// Read the current value (0%)
document.styleSheets[0].cssRules[6].cssRules[0].keyText;

// Change the value to 10%
document.styleSheets[0].cssRules[6].cssRules[0].keyText = '10%'

// Read the new value (10%)
document.styleSheets[0].cssRules[6].cssRules[0].keyText;

使用此功能,我们可以动态更改Web应用程序流中的动画关键帧,也可以响应用户操作。

访问@keyframes规则时可用的另一个属性是name:

1
2
3
4
5
6
7
8
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 7) {
p.innerHTML += `<code>${i.name}</code><br>`;
}
}

回想一下,在CSS中,@keyframes规则如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
@keyframes exampleAnimation {
from {
color: blue;
}

20% {
color: orange;
}

to {
color: green;
}
}

因此,name属性允许我读取以@keyframes和自定义名称该规则。animation-name特定元素上启用动画时在属性中使用的名称相同。

我在这里要提到的最后一件事是能够获取单个关键帧内的特定样式。这是一些带有演示的示例代码:

1
2
3
4
5
6
7
8
9
10
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 7) {
for (j of i.cssRules) {
p.innerHTML += `<code>${j.style.color}</code><br>`;
}
}
}

在这个例子中,在我找到@keyframes规则之后,我遍历关键帧中的每个规则(例如“from”规则,“20%”规则等)。然后,在每个规则中,我访问一个单独的style属性。在这种情况下,因为我知道color是在@keyframes定义的唯一属性。

这个例子的主要内容是使用style属性或对象。之前我展示了如何使用此属性来访问内联样式。但在这种情况下,我使用它来访问单个关键帧内的各个属性。

你可能会看到这允许您动态修改单个关键帧的属性,这可能会由于某些用户操作或应用程序或基于Web的游戏中发生的事件而发生改动。

#添加和删除CSS声明

CSSStyleSheet可以访问两种方法,允许您从样式表中添加或删除整个规则:insertRule()和deleteRule()。让我们看看在示例中,他们两个如何操作样式表:

1
2
3
4
5
let myStylesheet = document.styleSheets[0];
console.log(myStylesheet.cssRules.length); // 8

document.styleSheets[0].insertRule('article { line-height: 1.5; font-size: 1.5em; }', myStylesheet.cssRules.length);
console.log(document.styleSheets[0].cssRules.length); // 9

在这种情况下,我正在记录cssRules属性的长度(显示样式表最初有8个规则),然后我使用以下insertRule()方法将以下CSS添加为单独的规则:

1
2
3
4
article {
line-height: 1.5;
font-size: 1.5em;
}

我cssRules再次记录属性的长度以确认添加了规则。

该insertRule()方法将字符串作为第一个参数(这是必需的),该参数包括待插入的完整样式规则(包括选择器,花括号等),包括嵌套在at规则中的各个规则可以包含在此字符串中。

第二个参数是可选的。这是一个整数,表示您希望插入规则的位置或索引。如果未包括,则默认为0(意味着规则将插入规则集合的开头)。如果索引恰好大于规则对象的长度,则会引发错误。

该deleteRule()方法使用起来更简单:

1
2
3
4
5
let myStylesheet = document.styleSheets[0];
console.log(myStylesheet.cssRules.length); // 8

myStylesheet.deleteRule(3);
console.log(myStylesheet.cssRules.length); // 7

在这个例子中,这个方法接受一个参数,该参数表示我要删除的规则的索引。

使用任一方法,由于基于零的索引,作为参数传入的选定索引必须小于cssRules对象的长度,否则将引发错误。

#重新访问 CSS样式声明 API

之前我解释了如何访问声明为内联样式的单个属性和值。这是通过element.style,暴露CSS样式声明接口完成的。

CSSS样式声明API作为CSS样式表API的子集暴露一个单独的样式规则,当我向您展示如何访问@keyframes规则内的属性时,我已经提到了这一点。要了解其工作原理,请比较以下两个代码段:

1
<div style="color: lightblue; width: 100px; font-size: 1.3em !important;"></div>
1
2
3
4
5
.box {
color: lightblue;
width: 100px;
font-size: 1.3em !important;
}

第一个例子是获取内联样式的集合如下所示:

1
document.querySelector('div').style

CSS样式声明API,允许我获取element.style.color,element.style.width等属性对象。

但我可以在外部样式表中的单个样式规则上公开完全相同的API。这意味着我将我对style属性的使用与CSS样式表API结合起来。

因此,上面第二个示例中的CSS使用与内联版本完全相同的样式,可以像这样访问:

1
document.styleSheets[0].cssRules[0].style

这里打开一个外链样式表的一个样式规则的CSS样式声明对象,如果有多个样式规则,可以使用cssRules[1], cssRules[2], cssRules[3]等方式来获取。

因此,在外部样式表中,我可以访问前面提到的所有方法和属性。这包括SetProperty(),getPropertyValue(),item(),removeProperty()和getPropertyPriority()。除此之外,这些相同的功能可用于@keyframes或@media规则内的单个样式规则。

这是一个代码片段和演示,演示了如何在我们的示例样式表中的单个样式规则上使用这些方法:

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
// Grab the style rules for the body and main elements
let myBodyRule = document.styleSheets[0].cssRules[1].style,
myMainRule = document.styleSheets[0].cssRules[2].style;

// Set the bg color on the body
myBodyRule.setProperty('background-color', 'peachpuff');

// Get the font size of the body
myBodyRule.getPropertyValue('font-size');

// Get the 5th item in the body's style rule
myBodyRule.item(5);

// Log the current length of the body style rule (8)
myBodyRule.length;

// Remove the line height
myBodyRule.removeProperty('line-height');

// log the length again (7)
myBodyRule.length;

// Check priority of font-family (empty string)
myBodyRule.getPropertyPriority('font-family');

// Check priority of margin in the "main" style rule (!important)
myMainRule.getPropertyPriority('margin');

#CSS类型对象模型…未来?

在上面提过的所有内容之后,我不得不说在将来的某一天,我们所知道的CSSOM很可能也会过时。

那是因为有一个被称为CSS Typed OM的出现,它是Houdini项目的一部分。虽然有些人已经注意到新的Typed OM与目前的CSSOM相比更加冗长,但Eric Bidelman在本文中概述的好处包括:

  • 更少的错误
  • 算术运算和单位转换
  • 更好的性能
  • 错误处理
  • CSS属性名称始终是字符串

有关这些功能的完整详细信息和语法的一瞥,请务必查看完整的文章。

在撰写本文时,CSS Typed OM仅在Chrome中受支持。您可以在本文档中查看浏览器支持的进度。

#最后小结

通过JavaScript操作样式表肯定不是你在每个项目中都要做的事情。使用我在这里介绍的方法和属性实现的一些复杂交互有一些非常具体的用例。

如果您已经构建了某种使用这些API的工具,我很乐意听到它。我的研究只是触及了表面上的东西,但我很想知道在现实世界的例子中如何使用它。

我已将本文中的所有演示文稿放入CodePen集合中,因此您可以根据自己的喜好随意使用它们。

2019年写作计划

发表于 2019-03-03

目标

记录2019年写作计划。

计划

2019年2月

  1. JavaScript进阶之new Object的过程
  2. JavaScript进阶之模拟call和apply
  3. JavaScript进阶之继承

2019年3月

  1. [译]CSS对象模型(CSSOM)指南
  2. 跟underscore一起学如何写函数库
  3. 跟underscore一起学函数去重
  4. 跟underscore一起学节流和防抖
  5. 跟underscore一起学shuffle
  6. JavaScript进阶之OOP继承的姿势
  7. JavaScript进阶之函数式编程(curry)
  8. JavaScript进阶之手写Promise

2019年4月

//todolist

//可以介绍在前端用到的算法技巧

//可以介绍MVVM的实现方式:脏值检测,defineproperty,proxy

//promise async和await

//JavaScript设计模式之代理模式

//JavaScript设计模式之装饰器模式

//JavaScript进阶之重绘和回流

//BEM

JavaScript进阶之模拟call,apply和bind

发表于 2019-02-26 | 分类于 JavaScript进阶系列

目的

本文主要用于理解和掌握call,apply和bind的使用和原理,本文适用于对它们的用法不是很熟悉,或者想搞清楚它们原理的童鞋。好,那我们开始!
在JavaScript中有三种方式来改变this的作用域call,apply和bind。我们先来看看它们是怎么用的,只有知道怎么用的,我们才能来模拟它😸。

Function.prototype.call()

首先是Function.prototype.call(),不熟的童鞋请猛戳MDN,它是这么说的:call()允许为不同的对象分配和调用属于一个对象的函数/方法。也就是说:一个函数,只要调用call()方法,就可以把它分配给不同的对象。

如果还是不明白,不急!跟我往下看,我们先来写一个call()函数最简单的用法:

1
2
3
4
5
6
7
function source(){
console.log(this.name); //打印 xuan
}
let destination = {
name:"xuan"
};
console.log(source.call(destination));

上述代码会打印出destination的name属性,也就是说source()函数通过调用call(),source()函数中的this对象可以分配到destination对象中。类似于实现destination.source()的效果,当然前提是destination要有一个source属性

好,现在大家应该明白call()的基本用法,我们再来看下面的例子:

1
2
3
4
5
6
7
8
9
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
}
let destination = {
name:"xuan"
};
console.log(source.call(destination,18,"male"));

打印效果如下:

image-20190223204803524

我们可以看到可以call()也可以传参,而且是以参数,参数,...的形式传入。

上述我们知道call()的两个作用:

1.改变this的指向

2.支持对函数传参

我们看到最后还还输出一个undefined,说明现在调用source.call(…args)没有返回值。

我们给source函数添加一个返回值试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
console.log(source.call(destination,18,"male"));

打印结果:

image-20190223205338670

果不其然!call()函数的返回值就是source函数的返回值,那么call()函数的作用已经很明显了。

这边再总结一下:

  1. 改变this的指向
  2. 支持对函数传参
  3. 函数返回什么,call就返回什么。

模拟Function.prototype.call()

根据call()函数的作用,我们下面一步一步的进行模拟。我们先把上面的部分代码摘抄下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};

上面的这部分代码我们先不变。现在只要实现一个函数call1()并使用下面方式

1
console.log(source.call1(destination));

如果得出的结果和call()函数一样,那就没问题了。

现在我们来模拟第一步:改变this的指向。

假设我们destination的结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let destination = {
name:"xuan",
source:function(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
}

我们执行destination.source(18,"male");就可以在source()函数中把正确的结果打印出来并且返回我们想要的值。

现在我们的目的更明确了:给destination对象添加一个source属性,然后添加参数执行它。

所以我们定义如下:

1
2
3
4
5
6
Function.prototype.call1 = function(ctx){
ctx.fn = this; //ctx为destination this指向source 那么就是destination.fn = source;
ctx.fn(); // 执行函数
delete ctx.fn; //在删除这个属性
}
console.log(source.call1(destination,18,"male"));

打印效果如下:

image-20190224111229956

我们发现this的指向已经改变了,但是我们传入的参数还没有处理。

第二步:支持对函数传参。

我们使用ES6语法修改如下:

1
2
3
4
5
6
Function.prototype.call1 =function(ctx,...args){
ctx.fn = this;
ctx.fn(...args);
delete ctx.fn;
}
console.log(source.call1(destination,18,"male"));

打印效果如下:

image-20190224111709834

参数出现了,现在就剩下返回值了,很简单,我们再修改一下:

1
2
3
4
5
6
7
Function.prototype.call1 =function(ctx,...args){
ctx.fn = this || window; //防止ctx为null的情况
let res = ctx.fn(...args);
delete ctx.fn;
return res;
}
console.log(source.call1(destination,18,"male"));

打印效果如下:

image-20190224111912666

现在我们实现了call的效果!

模拟Function.prototype.apply()

apply()函数的作用和call()函数一样,只是传参的方式不一样。apply的用法可以查看MDN,MDN这么说的:apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。

apply()函数的第二个参数是一个数组,数组是调用apply()的函数的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
console.log(source.apply(destination,[18,"male"]));

image-20190224135505703

效果和call()是一样的。既然只是传参不一样,我们把模拟call()函数的代码稍微改改:

1
2
3
4
5
6
7
Function.prototype.apply1 =function(ctx,args=[]){
ctx.fn = this || window;
let res = ctx.fn(...args);
delete ctx.fn;
return res;
}
console.log(source.apply1(destination,[18,'male']));

执行效果如下:

image-20190224144414290

apply()函数的模拟完成。

Function.prototype.bind()

对于bind()函数的作用,我们引用MDN,bind()方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this对象,之后的一序列参数将会在传递的实参前传入作为它的参数。我们看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
var res = source.bind(destination,18,"male");
console.log(res());
console.log("==========================")
var res1 = source.bind(destination,18);
console.log(res1("male"));
console.log("==========================")
var res2 = source.bind(destination);
console.log(res2(18,"male"));

打印效果如下:

image-20190224201114865

我们发现bind函数跟apply和call有两个区别:

1.bind返回的是函数,虽然也有call和apply的作用,但是需要在调用bind()时生效

2.bind中也可以添加参数

明白了区别,下面我们来模拟bind函数。

模拟Function.prototype.bind()

和模拟call一样,现摘抄下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};

然后我们定义一个函数bind1,如果执行下面的代码能够返回和bind函数一样的值,就达到我们的目的。

1
2
var res = source.bind1(destination,18);
console.log(res("male"));

首先我们定义一个bind1函数,因为返回值是一个函数,所以我们可以这么写:

1
2
3
4
5
6
7
8
9
Function.prototype.bind1 = function(ctx,...args){
var that = this;//外层的this指向通过变量传进去
return function(){
//将外层函数的参数和内层函数的参数合并
var all_args = [...args].concat([...arguments]);
//因为ctx是外层的this指针,在外层我们使用一个变量that引用进来
return that.apply(ctx,all_args);
}
}

打印效果如下:

image-20190225230039054

这里我们利用闭包,把外层函数的ctx和参数args传到内层函数,再将内外传递的参数合并,然后使用apply()或call()函数,将其返回。

当我们调用res("male")时,因为外层ctx和args还是会存在内存当中,所以调用时,前面的ctx也就是source,args也就是18,再将传入的”male”跟18合并[18,’male’],执行source.apply(destination,[18,'male']);返回函数结果即可。bind()的模拟完成!

但是bind除了上述用法,还可以有如下用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};
var res = source.bind1(destination,18);
var person = new res("male");
console.log(person);

打印效果如下:

image-20190225230707230

我们发现bind函数支持new关键字,调用的时候this的绑定失效了,那么new之后,this指向哪里呢?我们来试一下,代码如下:

1
2
3
4
5
6
7
8
9
function source(age,gender){
console.log(this);
}
let destination = {
name:"xuan"
};
var res = source.bind(destination,18);
console.log(new res("male"));
console.log(res("male"));

image-20190225232709714

执行new的时候,我们发现虽然bind的第一个参数是destination,但是this是指向source的。

image-20190225232842678

不用new的话,this指向destination。

好,现在再来回顾一下我们的bind1实现:

1
2
3
4
5
6
7
8
9
Function.prototype.bind1 = function(ctx,...args){
var that = this;
return function(){
//将外层函数的参数和内层函数的参数合并
var all_args = [...args].concat([...arguments]);
//因为ctx是外层的this指针,在外层我们使用一个变量that引用进来
return that.apply(ctx,all_args);
}
}

如果我们使用:

1
2
var res = source.bind(destination,18);
console.log(new res("male"));

如果执行上述代码,我们的ctx还是destination,也就是说这个时候下面的source函数中的ctx还是指向destination。而根据Function.prototype.bind的用法,这时this应该是指向source自身。

我们先把部分代码抄下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function source(age,gender){
console.log(this.name);
console.log(age);
console.log(gender);
//添加一个返回值对象
return {
age:age,
gender:gender,
name:this.name
}
}
let destination = {
name:"xuan"
};

我们改一下bind1函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.bind1 = function (ctx, ...args) {
var that = this;//that肯定是source
//定义了一个函数
let f = function () {
//将外层函数的参数和内层函数的参数合并
var all_args = [...args].concat([...arguments]);
//因为ctx是外层的this指针,在外层我们使用一个变量that引用进来
var real_ctx = this instanceof f ? this : ctx;
return that.apply(real_ctx, all_args);
}
//函数的原型指向source的原型,这样执行new f()的时候this就会通过原型链指向source
f.prototype = this.prototype;
//返回函数
return f;
}

我们执行

1
2
var res = source.bind1(destination,18);
console.log(new res("male"));

效果如下:

image-20190225235357902

已经达到我们的效果!

现在分析一下上述实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//调用var res = source.bind1(destination,18)时的代码分析
Function.prototype.bind1 = function (ctx, ...args) {
var that = this;//that肯定是source
//定义了一个函数
let f = function () {
... //内部先不管
}
//函数的原型指向source的原型,这样执行new f()的时候this就会指向一个新家的对象,这个对象通过原型链指向source,这正是我们上面执行apply的时候需要传入的参数
//f.prototype==>source.prototype
f.prototype = this.prototype;
//返回函数
return f;
}

f()函数的内部实现分析:

1
2
3
4
5
6
7
8
9
10
11
//new res("male")相当于运行new f("male");下面进行函数的运行态分析
let f = function () {
console.log(this);//这个时候打印this就是一个_proto_指向f.prototype的对象,因为f.prototype==>source.prototype,所以this._proto_==>source.prototype
//将外层函数的参数和内层函数的参数合并
var all_args = [...args].concat([...arguments]);
//正常不用new的时候this指向当前调用处的this指针(在全局环境中执行,this就是window对象);使用new的话这个this对象的原型链上有一个类型是f的原型对象。
//那么判断一下,如果this instanceof f,那么real_ctx=this,否则real_ctx=ctx;
var real_ctx = this instanceof f ? this : ctx;
//现在把真正分配给source函数的对象传入
return that.apply(real_ctx, all_args);
}

至此bind()函数的模拟实现完毕!如有不对之处,欢迎拍砖!您的宝贵意见是我写作的动力,谢谢大家。

JavaScript进阶之new Object的过程

发表于 2019-02-21 | 分类于 JavaScript进阶系列

写在前面的话

前端的入门相对简单,相对于其他方向天花板可能会相对较低。但是在市场上一个优秀的前端依旧是很抢手的。能够站在金字塔上的人往往寥寥无几。

目前前端也已经一年半了,在公司的知识栈相对落后,就业形势不容乐观,所以有必要自己琢磨,往中高级前端进阶。后续我将推出《JavaScript进阶系列》,一方面是一个监督自己学习的一个过程,另一方面也会给看到的童鞋一些启发。

JavaScript新建对象的过程

在ES5中定义一个函数来创建对象,如下:

1
2
3
4
5
6
7
8
9
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){
return name;
}
var person = new Person("xuan");
console.log(person.name);//输出:xuan
console.log(person.getName());//输出:xuan

我们看到当我们新建一个对象,我们就可以访问构造器中的指向this的属性,还可以访问原型中的属性。我们不妨把JavaScript调用new的过程主要由下面四步组成:

  1. 新生成一个空对象
  2. 将空对象链接到原型中
  3. 绑定this
  4. 返回新对象

下面跟着我按照这个思路来创建对象:

1
2
3
4
function create(){
//Todo
}
person = create(Person,"xuan");//create(ObjectName,...arguments)

我们使用如上所示的函数来模拟new关键字。

首先第一步新建一个对象:

1
2
3
4
5
function create(){
var obj = new Object();
return obj;
}
person = create(Person,"xuan");

现在已经创建并返回一个对象,当然现在打印出来肯定是一个普通的对象,毕竟流程还没有走完,我们接着往下看。

第二步链接到原型中:

1
2
3
4
5
6
7
8
9
10
function create(){
var obj = new Object();
var constructor = [].shift.call(arguments);
console.log(constructor);
console.log(arguments);
obj.__proto__ = constructor.prototype;
return obj;
}

person = create(Person,"xuan");

image-20190221235358202

现在把构造函数和参数都打印出来了。没问题!

第三步绑定this,如下:

1
2
3
4
5
6
7
8
9
function create() {
let obj = new Object();
let constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
constructor.apply(obj, arguments);
console.log(obj);
return obj;
}
person = create(Person,"xuan");

image-20190222002407145

打印结果实现new对象的效果。

现在改一下构造函数代码:

1
2
3
4
5
6
7
8
9
function Person(name){
this.name = name;
return {
name:"abc"
}
}
var person = new Person("xuan");
console.log(person);
console.log(Object.prototype.toString.call(person));

效果如下:

image-20190222002825742

我们执行一下我们构建的函数效果如下:

image-20190222002932127

发现不一致,所以我们要处理第三步绑定this中apply函数的返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
function create() {
let obj = new Object();
let constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
//constructor.apply(obj, arguments);
let res = constructor.apply(obj, arguments);
if(res){
return res;
}else{
return obj;
}
}
person = create(Person,"xuan");

效果如下:

image-20190222003313618

完美!

现在我们思考一下这里的res返回值有三种情况:undefined,基本类型,对象。

如果res是undefined时,返回obj;

如果res是基本类型我们也返回obj;

如果res是对象我们返回res对象;

综合一下:

如果返回的res对象是Object类型那么返回res,否则返回obj。当然其他的判断条件也是可以的。最后代码优化如下:

1
2
3
4
5
6
7
8
9
function create() {
let obj = new Object();
let constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
//constructor.apply(obj, arguments);
let res = constructor.apply(obj, arguments);
return res instanceof Object?res:obj;
}
person = create(Person,"xuan");

几个问题

现在的代码已经完美了么?我们先来提几个问题。

  1. new Object()创建的对象纯净么?
  2. 为啥使用[].shift.call()来进行参数分割?arguments是一个数组么?

new Object()创建的对象纯净么?

首先什么是纯净?我们定义一个对象的__proto__属性为空的对象是一个纯净的对象。

在第二步的时候中已经改变的obj的原型链,所以无论它前面的原型链是咋样的都无所谓,但是为了保证对象的纯净性,我们有必要引出Object.create(),该方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。我们来看一下:

1
var person1 = Object.create({});

打印如下:

image-20190222192124090

我们看到person1的__proto__指向了{}对象,所以我们在上述代码中直接修改如下:

1
2
3
4
5
6
7
function create() {
let constructor = [].shift.call(arguments);
let obj = Object.create(constructor.prototype);
let res = constructor.apply(obj, arguments);
return res instanceof Object?res:obj;
}
person = create(Person,"xuan");

为啥使用[].shift.call()来进行参数分割?arguments是一个数组么?

首先我们知道arguments是函数传入的参数,那么这个参数是数组么?我们打印一下便知:

1
2
3
console.log(arguments);
console.log(Object.prototype.toString.call(arguments));
console.log(arguments instanceof Array);

结果如下

image-20190222193001990

不是数组。我们展开发现他跟数组很像,查一下资料发现这个对象是类数组。里面没有shift函数,直接调用shift会报错。我们使用使用Array.from(arguments)将arguments转成数组,然后在调用shift函数也是一种思路。但是在这里我们使用apply最适合。所以下述代码是模拟new Object()的最优代码:

1
2
3
4
5
6
7
function create() {
let constructor = [].shift.call(arguments);
let obj = Object.create(constructor.prototype);
let res = constructor.apply(obj, arguments);
return res instanceof Object?res:obj;
}
person = create(Person,"xuan");

还有更优的实现方法,请大佬们不吝拍砖!

模拟Vue的MVVM

发表于 2019-02-20 | 分类于 Vue

目的

为了真正的了解MVVM的核心思想,本文以Vue框架为切入点,手写模拟实现Vue框架的双向数据绑定,本文适合想进一步了解Vue底层的同学,当然对于想彻底搞懂MVVM原理的同学也会有所启发。

MVVM概念

image

MVVM的概念可以参考很多文章,比如这篇,其最核心的就是实现视图和视图模型的双向绑定。本文主要探究MVVM在Vue中的实现,下面将会围绕这个主题进行展开。

Object.defineProperty()

在讲解Vue的双向绑定之前,先来回顾一下Object.defineProperty(),在MDN中也有详细的解释,已经知道的同学可以跳过这个章,没看过的同学建议往下看。

1
2
3
var person = {};    
person.name = "xuan";
console.log(person);

上述代码在我们的日常开发中司空见惯,打印结果如下:

image

现在我们改写一下

1
2
3
4
5
6
7
8
var person = {};
Object.defineProperty(person,
"name",
{
value:"xuan"
}
);
console.log(person);

在IE8以上浏览器中打印结果如下:

image

一点问题没有。下面我们改一下这个对象属性,

1
2
3
4
5
6
7
8
9
var person = {};
Object.defineProperty(person,
"name",
{
value:"xuan"
});
console.log(person);
person.name = "xue";
console.log(person);

结果如下:

image

我们发现结果没有变化,直接使用”对象.属性”的方法已经失效,查一下API,在代码中添加writable属性

1
2
3
4
5
6
7
8
9
10
var person = {};
Object.defineProperty(person,
"name",
{
value:"xuan",
writable:true
});
console.log(person);
person.name = "xue";
console.log(person);

结果如下:

image

现在已经可以修改对象属性了,所以writable属性就是可复写的。在API中还存在configurable和enumerable属性,MDN这么说的:

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。

enumerable

当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

如果觉得不明白,看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var person = {}; 
Object.defineProperty(person,
"name",
{
value:"xuan",
writable:true,
configurable:true,
enumerable:true
});
console.log(person);
delete person.name;
console.log(person);
person.name = "xuan";
person.age = 18;
for(var key in person){
console.log(key);
}

结果如下:

image

简而言之:configurable 就是可删除的,enumerable就是可遍历的。在API中还有两个配置项set和get,废话不说,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 var person = {};
Object.defineProperty(person,
"name",
{
value: "xuan",
writable: true,
configurable: true,
enumerable: true,
set: function (newVal) {
console.log(`此处设置一个新值:${newVal}`)
this.value = newVal;
},
get: function () {
console.log(`此处获取一个值:${this.value}`)
return this.value;
}
});
console.log(person);

结果报错

image

上述错误的意思是我们要把writable和value配置项去掉。为什么?get和set方法是获取和设置值的函数,有了它们就不需要writable和value配置项了。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {};
Object.defineProperty(person,
"name",
{
configurable: true,
enumerable: true,
set: function (newVal) {
console.log(`此处设置一个新值:${newVal}`)
this.value = newVal;
},
get: function () {
console.log(`此处获取一个值:${this.value}`)
return this.value;
}
});
person.name = "xuan";
console.log(person);
console.log(person.name);

结果如下:

image

在设置和获取这个对象的属性时,都会打印相应的信息。一旦对象的信息有所修改,我们都能第一时间发现它,这就是\数据劫持**。当对象的值改变的时候,如果我们能够自动去更新对应的视图,就能够完成从\对象视图模型(VM)=>视图**的单向数据流绑定。

模拟Vue的数据劫持

在讲解数据劫持之前,先来看下面一段代码

1
2
3
<div id="app">
<p>姓名:{name}</p>
</div>
1
2
3
4
5
6
let vue = new Vue({
el:"#app",
data:{
name:"xuan"
}
});

这是一个最最基本的Vue程序,但是我们没有导入Vue的库,现在我们来看界面的效果:

image

现在我们没有看到任何效果,控制台还输出错误。好,我们现在只是搭建了一个空壳,下面我们一步步来实现和完善它。

劫持Vue中的data属性中的对象

我们先来定义一个Vue类,然后劫持传入配置项的data属性中的对象,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Vue(options={}){
this.$options = options;
let obj = this._data = options.data;
for(let key in obj){
var val = obj[key];
Object.defineProperty(obj,key,{
configurable:true,
enumerable:true,
set:function(newVal){
if(newVal === val) return;
console.log("数据已更新")
val = newVal;
},
get:function(){
console.log("数据已获取")
return val;
}
});
}
}

使用$options用于保存Vue的配置参数对象,_data用于存储真正的数据。我们打印下述信息:

1
2
3
console.log(vue);
console.log(vue._data.name="xue");
console.log(vue._data.name)

看一下效果:

image

我们需要的效果实现了!我们把代码整理一下:

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
let vue = new Vue({
el: "#app",
data: {
name: "xuan"
}
});
console.log(vue);

function Vue(options = {}) {
this.$options = options;
let data = this._data = options.data;
observe(data);
}

function observe(data) {
new Observe(data);
}

function Observe(data) {
var keys = Object.getOwnPropertyNames(data);
keys.forEach(function(key) {
var val = data[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function () {
return val;
},
set: function (newVal) {
console.log('获取数据属性 ', val, ' --> ', newVal);
val = newVal;
}});
}, this);
}

我们把数据劫持的代码逻辑写在Observe类中,我们下面就是围绕这个展开。

呃…现在我们的效果真达到了么?我们重新设置一下vue对象

1
2
3
4
5
6
7
8
9
10
11
let vue = new Vue({
el: "#app",
data: {
name: "xuan",
degree:{
name:"master",
time:"3year"
}
}
});
console.log(vue);

结果显示如下

image-20190220220513732

我们发现name属性还是有set和get方法,但是degree对象还嵌套着对象,所以这个时候我们需要递归遍历整个对象。于是添加修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Observe(data) {
if(typeof data!="object") return;
var keys = Object.getOwnPropertyNames(data);
keys.forEach(function(key) {
var val = data[key];
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function () {
console.log('获取数据属性',val);
return val;
},
set: function (newVal) {
console.log('设置数据属性 ', val, ' --> ', newVal);
val = newVal;
}
});
}, this);
}

主要修改两个地方,在var val = data[key]后添加observe(val),对获取的属性值进行观察,然后在设置递归终止条件, if(typeof data!=”object”) return;最后效果如下:

image-20190220220624044

下面再来写一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
let vue = new Vue({
el: "#app",
data: {
name: "xuan",
degree: {
name: "master",
time: "3year"
}
}
});
vue._data.name = {a:1}
console.log(vue);

结果如下:

image-20190220225340987

我们发现name属性其实已经被劫持到了,但是传进去的对象没有被劫持到,所以在set方法里面也需要添加observe函数。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Observe(data) {
if(data==null || typeof data!="object") return;
Object.keys(data).forEach(function(key){
var val = data[key];
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: function () {
console.log('获取数据属性',val);
return val;
},
set: function (newVal) {
console.log('设置数据属性 ', val, ' --> ', newVal);
val = newVal;
observe(val);
}
});
});
}

结果如下:

image-20190220225646064

上述基本完成了对于配置对象中data属性的数据劫持。

数据代理

## References

界面之下:还原真实的MV*模式

JS的GC整理

发表于 2018-12-26 | 分类于 JAVASCRIPT

内存泄露

什么是内存泄露?

内存泄露就是应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。这句话对于所有需要GC的语言都适用,当然包括JS。

JS的内存回收方法主要就是“标记清除”和“引用计数”。

如何判断内存泄露?

经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。

查看内存可以使用chrome浏览器中的开发者工具,Performance页签可以进行内存快照的监控。

内存泄露的一般场景是什么?

1 意外的全局变量

2 被遗忘的计时器或回调函数

3 脱离 DOM 的引用

4 闭包

WeakSet 和 WeakMap

WeakSet 和 WeakMap就是弱引用,在java中也存在这个概念,就是该数据结构不会影响引用变量的计数次数。

算法的目标

发表于 2018-09-10 | 分类于 算法

目标

1.对遇到的特殊问题要能够自己设计出算法实现;

2.对于原理公开的知名算法,要能将算法原理翻译成具体的算法代码;

3.对已有具体实现的算法,要能够设计出合适的数学模型,将算法应用到实际问题中;

12…4
ZheHuaXuan

ZheHuaXuan

知足.感恩.幸运

31 日志
19 分类
45 标签
GitHub E-Mail
© 2019 ZheHuaXuan
由 Hexo 个人专属
|
主题 — NexT.Gemini v5.1.3