再认识构造函数、返回值和new关键字

基本概念

首先要明确和构造函数相关的基本概念,不要觉得概念定义这些都不重要,只要能自己能理解就ok,有时候分不清这些会让自己显得很不专业。

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
var Person = function(){
// age,showAge: 分别称为实例属性,实例方法(参考 《JavaScript高级程序设计v3》中的翻译)
// 因为这样定义的属性和方法,所有实例都会独立copy一份
this.age = 20;
this.showAge = function(){
alert(this.age);
}
// _foo,_foo2: 私有属性和方法,开头下划线是比较常见的约定
var _foo = 'foo';
function _foo2(){
//...
}
}
// job,showJob: 原型属性和方法
Person.prototype = {
job: 'FE',
showJob: function(){
alert(this.job);
}
}
//bar,bar2: 静态属性和方法
Person.bar = 'baz';
Person.bar2 = function(){
//...
}

以上命名都是传统静态语言中的命名对应到js中的叫法。

返回值问题

正常情况下,使用构造函数时是不会使用return语句的,但是如果不小心或者刻意加入return语句,会怎样呢?直接上实验结果:

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
//没有返回值
function Test0(){
this.name='test0';
}
var test0=new Test0;
alert(test0); //输出[Object]
alert(test0.name); //输出test0
//return 一个对象字面量
function Test(){
this.name='test';
return {}; // 返回对象字面量
}
var test=new Test();
alert(test); //输出[Object]
alert(test.name); //输出undefined,说明构造函数创建的对象是返回的空{},this指向未生效。
//return 一个对象字面量
function Test1(){
this.name='test';
return []; // 返回对象字面量
}
var test1=new Test1();
alert(test1); //输出[]
alert(test1.name); //输出undefined,说明构造函数创建的对象是返回的空[],this指向未生效。
//return 一个字符串对象
function Test2(){
this.name='test';
return new String('123'); // 返回字符串对象
}
var test2=new Test2();
alert(test2); //输出123
alert(test2.name); //输出undefined,说明构造函数创建的对象是字符串对象
//return 一个原始类型字符串
function Test3(){
this.name='test2';
return '123';// 返回字符串对象
}
var test3=new Test3();
alert(test3); //输出[Object]
alert(test3.name); //输出test0

总结就是,如果返回值是引用类型数据,则使用返回值,否则使用正常构造函数创建绑定流程,return new String('123')这种形式生成的其实也是object(可使用typeof new String()验证)。

至于为什么是这样的表现,只能说规范是这么定的,说不准以后就改了。

new关键字

new 关键字究竟干了什么?结合上面梳理的基本概念来理解,new 大体上干了三件事:

1
2
3
4
5
6
var obj = {}; //创建一个空对象
obj.__proto__ == Person.prototype; //构造函数的原型对象指向obj.为的是obj能继承原型属性或方法。
Person.call(obj). //将Person的this指向为obj,并执行构造函数,为的是obj能继承实例属性或方法。

之前就看到过相关博文的分析,然而很快就忘了,记得不够深刻。。。究其原因,还是对构造函数的相关概念理解不深刻导致的,同时充分说明了,要抠细节,一定要对各个语法概念有一个明确、清晰的认识。

取消浏览器记录滚动条位置

最近同事开发的页面遇到一个顽固的坑:
第一次访问某个页面,滚动一下,再刷新页面,发现滚动条停留在上次那个位置。

浏览器帮我们完成了这件事,本来初衷是好的,为了更好的用户体验嘛。然而有时候我们有一些特殊需求的页面,需要不记录而是每次进入都在文档顶部。这就苦了我们开发者了。记得以前通过js来重置。

比如进入页面后,js在onload事件中执行window.scrollTo(0,0)document.body.scrollTop = 0;然而现在都统统无效(在桌面chrome ,iOS浏览器测试),可能是浏览器升级导致的。

尝试了各种办法,最终找到解决办法:

1
2
3
4
5
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}else{
window.onunload= () => window.scrollTo(0,0);
}

解释:

  • history.scrollRestoration是新增的一个属性,值为auto|manual,默认为auto记录滚动条位置, 设为manual时就禁用记录位置的功能。兼容性不乐观(至少iOS 10.3.2版本的浏览器不行);

  • 所以就用到了onunload事件(关键),退出页面的时候滚动条位置重置为0,不得不说这个方法很巧妙666………

VSCode中诡异的emmet提示

问题

最近想体验一下VSCode(颜值高,功能全,资源占有少)。

然而刚上手就遇到非常不爽的问题,自带的emmet插件自动补全css老得不到我想要的:

img

输入mt,按tab补全出来mask-type而不是margin-top,输入c得到caption-side而不是color…真够蛋疼的。

Google了好久才发现是因为跟VSCode自带的默认智能提示冲突了,自带智能提示,也是用的tab键,由于提示优先级比emmet更高的原因,所以得不到我想要的结果。

解决办法

修改键盘快捷方式,将选中自带智能提示的键值改为enter即可解决。具体步骤为:首选项->键盘快捷方式(⌘k⌘s),然后找到并打开keybinding.json,在右侧提供的覆盖设置区域键入下面设置:

1
2
3
4
5
6
7
8
9
10
{
"key": "enter",
"command": "acceptSelectedSuggestion",
"when": "editorTextFocus && suggestWidgetVisible"
},
{
"key": "tab",
"command": "editor.emmet.action.expandAbbreviation",
"when": "config.emmet.triggerExpansionOnTab && editorTextFocus && !editorHasSelection && !editorReadonly && !editorTabMovesFocus"
}

再回首CSS3动画

众所周知CSS3动画性能手段的铁律之一就是,尽量不要使用会导致重排重绘的属性,之前看到这个后,就囫囵吞枣记住了,之后都会使用这个手段来优化性能,然而每次被人问起为什么要使用transform变换代替top/left等时,我都会说因为transform不会导致重绘…, 事实上原理并不是太清楚。

经过文档查阅,浏览器渲染CSS的步骤其实为:

Recalculate Style -> Layout -> Paint Setup and Paint -> Composite Layers
即:查找并计算样式 -> 排布 -> 绘制 -> 组合层

注意:

  • 查询属性会导致重排。
  • 最后这个组合层,其实类似于PS中的图层概念, 浏览器中的元素通常也是有多个层的,经过一系列计算后,还要经过GPU组合图层才渲染出页面的。
  • 重排必然导致重绘,重绘不一定触发重排。

为什么使用transform可以优化性能:

transform属性改变,发生在Composite Layers这一层,所以不会经过前面的重排,重绘;并且还会触发GPU加速。从而大大提升性能。

transform属性类似的还有opacity,也是直接在Composite Layers这一层处理透明度。

其他优化手段

  • 直接在元素上可以使用box-shaodw属性做动画:

    See the Pen Button hover effects with box-shadow by Giana (@giana) on CodePen.



    需要注意的是,直接在DOM元素上改变box-shaodw性能不如在元素的伪类上改变box-shaodw属性。参见这里(配合chrome开发者工具的timeline可以明显查看性能差异)

  • 浏览器有图层概念,一个’图层’上可以有N个DOM元素,而且如果图层中某个元素需要重绘,那么整个图层都需要重绘。 比如gif图渲染每一帧,都会导致所在图层重绘,那就应该将gif元素独立出来达到优化目的。

    附:Chrome中满足以下任意情况就会创建图层:

    1. 3D或透视变换(perspective transform)CSS属性
    2. 使用加速视频解码的<video>节点
    3. 拥有3D(WebGL)上下文或加速的2D上下文的<canvas>节点
    4. 混合插件(如Flash)
    5. 对自己的opacity做CSS动画或使用一个动画webkit变换的元素
    6. 拥有加速CSS过滤器的元素
    7. 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)
    8. 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

SPA页面路由的实现原理

面试被问到spa单页应用的router原理是什么?一下就不知道怎么回答,很久前看到锅相关问题,大致是运用了HTML5中history API的pushstate,由于工作中用得少(都是直接上现成的第三方Router库)就没太关注这个。
现在总结一下,前端单页应用的router实现无非是:

pushState

检测浏览器的history对象是否拥有pushState方法,有则直接使用,语法如下:

1
history.pushState(state, title, url);

参数有三个,第2个是历史遗留问题现已忽略,一般传null,其他两个:

  • state:状态对象,作用是可以保存历史数据,因为也是存储在本地磁盘中,可以理解为localStorage等。但是区别是字符大小有限制,超过640kb会报错。通过此参数保存的数据,获取方式为history.state;
  • url变更后的url,这个就是想要跳转到的页面地址,注意点是,必须跟当前url同域;还不仅支持hash跳转,同域下的任意url都行(这也是前端实现SPA路由去“#”号的原理,需服务器端配合)。

location.hash

如果不支持pushState方法, 就直接使用window.location.hash属性更改hash值(路由),同时监听window.onhashchange事件,做跳转后的操作。

1
2
3
window.location = '#foo';
window.location.hash = '#foo';
window.location.hash = 'foo';

以上三种写法在浏览器Chrome都能生效。

注意

请注意pushState()方法绝不会导致hashchange 事件被激活,即使新的URL和旧的只在hash上有区别。

参考

History.pushState() - Web API 接口 | MDN

js代码中的奇技淫巧

js老司机会写各种新手看不懂的代码,很倔很神奇的同事,性能也会更好。当然心思全花在这上面就本末倒置了。。。
下面都是我看过的,使用过的一些“奇技淫巧”(只是我认为,不一定是):

使用void 0代替undefined:

1
2
//三目运算这么写:
foo?true:void 0;

效果有:

  • 防止undefined在非严格模式下被重写;
  • void 0相比undefined可以节省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
26
27
//取整
parseInt(a,10); //Before
Math.floor(a); //Before
a>>0; //Before
~~a; //After
a|0; //After
//四舍五入
Math.round(a); //Before
a+.5|0; //After
//内置值
undefined; //Before
void 0; //After, 快
0[0]; //After, 略慢
//内置值
Infinity;
1/0;
//布尔值短写法
true; //Before
!0; //After
//布尔值短写法
false; //Before
!1; //After

生成一个重复数组,长度为m,值为n (只考虑n为string类型的情况).

1
const generateRepeatArr = (m, n) => new Array(m+1).join(n+Symbol).split(Symbol).slice(0,-1);

这几天面试过程中的思考

最近很意外的,简历居然通过了阿里的筛选,电话面试过程中面试官对简历上的描述也很有针对性的问了些问题,其中一条问”你说自己对UX有浓厚的性趣,可以举个例子吗”,当时没答得太好,下来想想,大致有以下几点:

  • 对网页设计比较重视,遇到设计漂亮美观的网站,都会“仔细研究一番”;
  • 遇到好的网页字体,都会下意识打开开发者工具,查看使用的什么字体;
  • 非常喜欢交互设计比较优秀的网页,App等, 不好的都会忍不住吐槽(比如一个按钮的位置对用户不友好,一个不合理的布局);
  • 经常去codepen欣赏优秀开发者的作品,获取灵感;

浅谈web安全

科技的进步,互联网的日益繁荣,方便了人们的同时,也带来了安全隐患。作为前端工程师,我们应该注意到基本的web安全知识

尾递归与递归问题

今日看技术blog,看到一个新词汇“尾递归”,发现了递归不为人知的一面。
概括一下就是,普通的函数递归调用可能会导致性能问题,比如常见的栈溢出。

以常见的阶乘算法为例:

1
2
3
4
5
6
7
8
function factorial(n){
if(n == 1){
return n;
}
else{
return n*factorial(n-1);
}
}

这是我经常写的、再熟悉不过的递归,然而编程中并不建议这种写法,甚至是唾弃,因为这个函数返回值是一个当前上下文中的变量与函数本身的乘积,这样的话,每次执行完此函数,上下文并没有被垃圾回收机制回收,一直常驻着,加上递归循环后风险非常大。这时尾递归显而易见成为了针对这个问题的优化手段。

先引入一个尾调用概念:

尾调用是指某个函数的最后一步是调用另一个函数,除此之外没有进行任何其他操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 情况1
function f(x){
return g(x);
}
// 情况2
function f(x){
let y = g(x);
return y;
}
// 情况3
function f(x){
return g(x) + 1;
}

以上只有情况1属于尾调用,因为进行了其他操作,所以2和3都不属于尾调用。
那么尾递归的定义,相比就清楚了:尾调用自身,就称为尾递归。

优化后的阶乘写法:

1
2
3
4
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

尾递归优化,其实就是递归函数优化为尾调用形式的这个过程。

参考文章:

尾调用优化-阮一峰

百度百科词条”尾递归“

js函数、变量提升的顺序问题

很早前就知道JavaScript语言有变量提升的特性,也有函数提升的特性(英文Hoisting),恰好今日的一个电话面试,面试官问道这个问题,暗自窃喜,然而没想到的是,他问我如果两者同时存在的时候,是先提升函数还是变量呢?一下就懵逼了,之前真没考虑过这个问题啊。。。

下来后,立即做了一个实验:

1
2
3
4
5
console.log(foo);
var foo = 'Hello World!';
function foo(){
//balabala...
}

或者

1
2
3
4
5
console.log(foo);
function foo(){
//balabala...
}
var foo = 'Hello World!';

结果打印出的都是function;

结论:由此可见,js中的函数或者变量提升特性,是先提升函数,再提升变量,(都是基础不扎实的锅啊😔)