CMeUp

记录一些学习心得,QQ:1139723651.

关于继承

看各种资料,介绍继承,从这么几方面来说:

1. 原型继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//继承 Father
Son.prototype = new Father(); // Son.prototype被重写,
// 导致Son.prototype.constructor也一同被重写
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue()); // true

问题:当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;并且这种继承硬编码的方式导致父类不能接收参数。

2. 借用构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function Father(){
this.colors = ["red","blue","green"];
}
function Son(name){
Father.call(this); // 继承了Father,且能向父类型传递参数
this.name = name;
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"

var instance2 = new Son();
console.log(instance2.colors); // "red,blue,green" 可见引用类型值是独立的

解决了父类引用类型数据被共享的问题,但是没有继承父类的原型方法。

3. 组合继承

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
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name, age){
Father.call(this, name); // 继承实例属性,第一次调用Father()
this.age = age;
}
Son.prototype = new Father(); // 继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // louis
instance1.sayAge(); // 5

var instance1 = new Son("zhai",10);
console.log(instance1.colors); // "red,blue,green"
instance1.sayName(); // zhai
instance1.sayAge(); // 10

解决了父类引用类型变量被共享和不能继承原型方法的问题。但是继承父类方法的时候,调用 new Father () 时,将 Father 的实例属性添加到 Son 的原型中,而这些属性在构造借用时已经添加到 Son 实例上了(相当于 Son.prototype 上这些 Father 的属性重复且无用)

4. 寄生组合式继承

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
function extend(subClass, superClass){
var prototype = Object.create(superClass.prototype); // 创建对象
prototype.constructor = subClass; // 增强对象
subClass.prototype = prototype; // 指定对象
}

function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}

Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name, age){
Father.call(this, name); // 继承实例属性,第一次调用Father()
this.age = age;
}
extend(Son,Father); // 继承父类方法,此处并不会第二次调用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // louis
instance1.sayAge(); // 5

var instance1 = new Son("zhai", 10);
console.log(instance1.colors); // "red,blue,green"
instance1.sayName(); // zhai
instance1.sayAge(); // 10

这样是最优解。

疑问

  • 为什么原型继承那里不用 subClass.prototype = superClass.prototype?
    其实因为如果这样做的话,对对象进行 instanceOf 的时候就会出问题。比如我 new 了一个 superClass 对象 super1,但中间执行过 extend (subClass, superClass) 这个方法,那么子类的 proto 就指向了 super 的 proto。造成的结果是 super1 instanceOf superClass === true(没毛病),super1 instanceOf subClass === true (瓦特?)。所以中间用一个过渡的对象,防止这种情况的发生。

JS 闭包–一点点心得体会

相信熟悉 js 的童鞋都了解、经常使用闭包了吧?但对于我来说,也只停留在熟悉、使用上,要问我为什么会产生闭包,我也只能笼统地说:内层作用域可以访问到外层作用域的变量。究其根本也不甚了解。今天有时间来分享一下这一两天的相关学习。

起因

万事有果皆有因,要问我为什么突然要学习 js 闭包,其实是下面一个问题:

1
2
3
4
5
6
7
8
9
10
11
function a() {
var variableInA = 'A variable in [[scope A]]';

new Promise((res, rej) => {
setTimeout(() => {
res('pass after setTimeout with 1000ms');
}, 1000);
}).then((data) => {
console.log('data in then: ', data, 'variableInA is:', variableInA);
});
}

从函数的角度来说,执行 a () 是一个同步的运行方式,运行完 new Promise ().then () 之后就会将 a () 创建的环境销毁(没有闭包的情况下)。我之前所理解的闭包是函数 a 运行返回一个函数 b,函数 b 中保持了 a 中的变量引用,那么函数 a 所创建的环境就不会被销毁(我也不太清楚是被引用的变量不会被销毁,还是说所有都不会 更新:2018-11-01 17:26:28:只有被引用了的才会产生闭包,应该是在执行前解释的时候决定的。但是讲道理只保留被引用的变量能最大程度地节省内存,不过对 gc 的要求就更多,需要去判断哪些被引用,哪些被间接地引用等等,chrome 的 V8 貌似属于只保留引用变量的那种)

b b

从上面两图来看,前图是只引用了一个变量 a,所以闭包中只有 a;后图中 a 是一个函数,引用了变量对象中的 b,所以 fn 函数的闭包中既有 a 又有 b。验证成功。大家可自行把玩 chrome 的 source 面板。

回到主题,这里的 Promise 并不是被返回的,而只是一个执行的步骤,但确实保留了对 a 创建的 variableInA 的引用,成为了所谓的闭包。不解。so 开始学习。

经过

首先了解了几个概念:变量对象、作用域链、调用栈。

变量对象

变量对象就是创建一个新的块级作用域时会先产生的一个对象,如下图:

b

在匿名调用 foo 函数后,进入了 foo 函数(此时还没开始执行代码),然后看红框,Local,这个可以理解成变量对象,此时函数内所有声明的变量、函数都会被列出来,var 有变量提升,funciton 有优先解释的权利,所以 Local 中 a、b 被提前声明,但值未赋,function 被提至最前。随着代码执行,逐一被赋值。这个就是变量对象。

作用域链

借上图,红框上面有 5 个大字(母),Scope,对,就是这个东西,作用域链。就是查找一个变量、函数啥的时候,由里到外,由上到下(针对 chrome 浏览器给出的这种 Scope 的顺序)地查找。当在某一层查找到了,就不往外再查找了;都查不到就返回 undefined。这就是作用域链。and,还有一个 problem,就是查得越深,就越慢。需要 notice 一下。

调用栈

还借上图,红框上面的上面有 9 个大字(母),call stack,对,就是这个东西,调用栈。它表示了函数的调用顺序。本例中是 windows 全局中匿名调用 foo。当调用开始或者结束的时候,call stack 会将当前环境压入或者弹出。

闭包

有了上面三个概念,这个就好说了:闭包就是 - 函数 - 保留有 - 其定义所在位置的(词法作用域)- 变量对象 - 的引用。没错,是这样的!为了方便断句和理解,我加了中划线。在前面的图中,由于 fn 指向了 baz 函数,导致 baz 函数在执行时,保留了 foo 函数执行时的变量对象(中的属性 a)的引用。

结果

上面一句话把闭包总结出来了,其实不知道这么说严不严格,但这对我来说很好理解。也让我想起了平时中一些经常使用但没细想的东西,打个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function a() {
var inputEle = document.getElementById('inputName');

setTimeout(() => {
console.log('Now start in timeout');
inputEle.value = 666;
}, 0);
console.log('Now end a().');
}
a(); // 执行a
/*
这里setTimeout就是在执行a()时创建(定义)的,所以在执行setTimeout中的匿名回调函数(注意,这是个函数,所以)保留了a执行时的活动对象,其中保留了对inputEle属性的引用,这就是闭包的使用。
*/

再次强调,闭包中的引用,就是 js 中真正意义上的引用。对于原始值是简单地复制,对于引用对象就是直接引用。所以利用闭包也可以方便地实现单例模式。

es6 中被我遗忘的点

  1. 变量解构可以嵌套很多层,但平时总是习惯解构一层,再手动再解构一层。
  2. 模板字符串处理可以使用 ${} 之外,还有一个标签模板的功能,举个比方:
    1
    2
    let name = 'NB_tzx';
    alert`My name is ${name}`
    就是说 ` 之间的会先解释,再将这个作为前面标签(函数)的输入。以前没遇到过。。
  3. 对超过 2 字节的字符的支持。fromCodePoint (), codePointAt (), at ()
  4. 正则中 sticky 沾粘,还有点的全匹配,及 s 标志。增加了后行断言(?<=)(?<!),还有具名分组
    1
    2
    3
    4
    5
    const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
    const matchObj = RE_DATE.exec('1999-12-31');
    const year = matchObj.groups.year; // 1999
    const month = matchObj.groups.month; // 12
    const day = matchObj.groups.day; // 31
    于是可以用解构赋值来提取匹配的值。

关于没传值时直接报错的 ES6 方式

1
2
3
4
5
6
7
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()

Symbol

Symbol 主要作用是防止对象的属性被无意地覆盖。通过保留对 let a = Symbol () 的引用,可以访问 obj [a]。通过使用 Symbol.for () 和 Symbol.keyFor (),方便对属性值的存取。有几个小地方需要注意,在使用 instanceof 的时候,首先会调用对象的 [Symbol.hasInstance] 方法,for of 循环调用的是对象的 [Symbol.iterator] 方法。

Promise

1
2
3
4
5
6
7
8
9
10
11
async function test(){
if (Math.random !== 0.5) {
return 1;
}

var b = await new Promise((res, rej)=> {
res(999)
})
return b;
}
// 我就想故意返回1,哈哈哈

我以为会直接返回 1,而实际上返回的也是一个 Promise,只不过这个 Promise 是已经 resolve 了的。

es5 中被我遗忘的点

属性描述符

1
2
3
// ES5中使用Object.defineProperty/defineProperties定义属性时,可以使用value/writable/configurable/enumerable或者get/set/configurable/enumerable
// 使用ES5的方式来定义属性时,默认writable/enumerable/configurable都是false,分别表明不能被等号=改写值、不能被for in枚举、不能被重新配置(配置和删除)
// ES3的字面量或者obj.property定义的所有描述符默认为true。

Object.freeze 方法

冻结一个对象,不能向其添加新的属性,不能修改其已有属性的值,不能删除已有的属性,不能修改已有属性的 enumerable/configurable/writable 属性;isFrozen 返回是否是冻结对象。

Object.seal 方法

密封一个对象,不能添加新属性,已有属性不可配置,但已有属性的值可以修改。isSealed 判断是否被密封。

Object.preventExtensions

使一个对象不能再添加新的属性(仅阻止添加自身的属性。但属性仍然可以添加到对象原型)。

关于 try/catch/finally

1
2
3
4
5
6
7
8
9
function a(){

try {
return 123;
} finally{
console.log('finally');
}
}
console.log(a()); // 打印什么内容?

首先看一下 es5 中对 try/finally 的定义:

The production TryStatement : try Block Finally is evaluated as follows:

  1. Let B be the result of evaluating Block.
  2. Let F be the result of evaluating Finally.
  3. If F.type is normal, return B.
  4. Return F.

由上定义,可知,try 和 finally 中的代码都会执行。只是看返回哪一个的问题了。因此,上面的会打印:finally 123.

关于 background-position

以前吧,一直觉得这个具体值和百分比值的作用是一样的。其实是不一样的。画几个图来说明。
关于 background-position
意思即是:具体值(如 10px 0)是指背景到容器左边缘(没考虑其他文档流的影响)的距离。为正是在左边缘的右边,反之。

百分比是指背景的百分比值处与容器的百分比值处对齐(正值的时候)。负值的时候,使用下面的公式计算(正值也可以计算):

1
2
3
(容器宽度 - 背景宽度) * 百分比X = 像素值X
(容器高度 - 背景高度) * 百分比Y = 像素值Y
这里得到的X 和 Y就是背景的起始位置。正代表在容器左边缘的右边,反之。
0%