JS 原型链

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性([[Prototype]])指回构造函数,而实例有一个内部指针指向原型。

如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

function SuperType() {
    this.property = true;
}
SuperType.protype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
    return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true;

JS this

this关键字是JS中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。

为什么要使用this

this提供了一种更优雅的方式来隐式 ”传递“ 一个对象引用,因此可以把API设计得更加简洁且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用this则不会这样。

this是在运行时进行绑定的,他的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

this既不指向函数自身也不指向函数的词法作用域,this实际上是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

调用位置

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log("baz");
    bar();
}
function bar() {
    // 当前调用栈是:baz - bar
    // 因此,当前调用位置是baz
    console.log("bar");
    foo();
}
function foo() {
    // 当前调用栈是:baz - bar - foo
    // 因此,当前调用位置是bar
    console.log("foo");
}
baz();

绑定规则

函数的执行过程中调用位置如何决定this的绑定对象:你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。

默认绑定

最常见的函数调用类型:独立函数调用。

function foo() {
    console.log(this.a);
}
var a = 2;
foo(); // 2

当调用foo时,this.a被解析成全局变量a。为什么?因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象。

怎么知道是应用默认绑定?可以通过分析调用位置。在代码中,foo是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

如果使用严格模式,则不能将全局对象用于默认绑定,this会绑定到undefined

function foo() {
    "use strict"
    console.log(this.a);
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

这里有一个微妙但非常重要的细节,虽然this的绑定规则完全取决于调用位置,但只有foo运行在非严格模式下时默认绑定才能绑定到全局对象,在严格模式下调用foo不影响绑定。

function foo() {
    console.log(this.a);
}
var a = 2;
"use strict"
foo(); // 2

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说被某个对象拥有或者包含。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

foo 被调用时,他的前面加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因此,this.aobj.a 是一样的。

对象属性引用链中只有最后一层在调用位置中起作用。

function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 2,
    foo: foo
};
var obj1 = {
    a: 1,
    obj2: obj2
};
obj1.obj2.foo(); // 2

隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定,从而把this绑定到全局对象或者undefined上。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo;
var a = 3;
bar(); // 3

一种更微妙、更常见、更出乎意料的情况发生在传入回调函数时:

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    fn();
}
var obj = {
    a: 2,
    foo: foo
}
var a = "global";
doFoo(obj.foo); // "global" , 无论全局变量a是在obj之前还是之后声明并赋值

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。如果把函数传入语言内置函数而不是传入自己声明的函数,结果也是一样的。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
var a = "global";
setTimeout(obj.foo, 100); // "global"

如上,回调函数丢失this绑定是非常常见的,此外,调用回调函数还可能会修改this

显式绑定

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

具体来说,可以使用callapply方法。这两个方法如何工作呢,他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。

function foo() {
    console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 2

可惜,显示绑定仍然无法解决丢失绑定的问题。

硬绑定

硬绑定可以解决丢失绑定的问题。

我们创建函数bar并在内部手动调用foo.call(obj),因此强制把foothis绑定到了obj,之后无论如何调用函数bar,他总会在obj上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

function foo() {
    console.log(this.a);
}
var obj = { a: 2 };
var bar = function() {
    foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的bar不可能再修改this
bar.call(window) // 2

硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = { a: 2 };
var bar = function() {
    return foo.apply(obj, arguments);
}
var b = bar(3); // 2 3 
console.log(b); // 5

另一种是创建一个可以重复使用的辅助函数

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    };
}
var obj = { a: 2 };
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); //5

由于硬绑定是一种非常常用的模式,所有ES5提供了内置的方法Function.protoprototype.bind

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
var obj = { a: 2};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

new 绑定

JS中,构造函数只是一些使用new操作符时被调用的函数,他们并不会属于某个类,也不会实例化一个类。实际上,他们甚至不能说是一种特殊的函数类型,他们只是被new操作符调用的普通函数而已。

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

使用new来调用foo时,我们会构造一个新对象并把它绑定到foo调用中的this上。

优先级

如果某个调用位置应用了多条规则怎么办?为了解决这个问题就必须给这些规则设定优先级。

毫无疑问,默认绑定的优先级是四条规则中最低的,new绑定比显式绑定优先级高。显示绑定的优先级比隐式绑定高。

JS 闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包的效果

函数bar的词法作用域可以访问foo的词法作用域。在foo执行后,通常会期待foo的整个内部作用域被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。而闭包可以阻止这件事情的发生。

bar所声明的位置所赐,它拥有涵盖foo内部作用域的闭包使得该作用域能够一直存活,以供bar在之后任何时间进行引用。

bar依然持有对该作用域的引用,这个引用就叫做闭包

本质上无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

function wait(message) {
    setTimeout(() => console.log(message), 1000);
}
wait("Hello Closure"); 

将一个匿名函数传递给setTimeout,内部函数具有涵盖wait作用域的闭包,因此保留对变量message的引用。

循环与闭包

for (var i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), i * 1000);
}

正常情况下,我们对这段代码的预期是分别输出0-4,每秒一次,一次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次5。

5从哪里来?循环终止条件是i < 5, 条件首次成立时i的值为5,因此,输出显示的是循环结束时i的最终值。

仔细想一下,延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(…, 0),所有的回调函数依然是在循环结束后才会被执行。

为什么代码的行为跟语义所暗示的不一致?

我们试图假设循环中的每个迭代在运行时都会给自己捕获一个i的副本,但根据作用域的工作原理,实际情况是尽管循环中五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i

缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

IIFE通过声明并立即执行一个函数来创建作用域。

for (var i = 0; i < 5; ++i) {
    (function(j) { // IIFE需要有自己的变量在每次迭代中存储i的值
        setTimeout(function timer() { console.log(j); }, j * 1000); 
    })(i);
}

在迭代中使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

块作用域

我们使用IIFE在每个迭代都生成一个新的作用域,换句话说,每次迭代都需要一个块作用域。let声明可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

for (var i = 0; i < 5; ++i) {
    let j = i;
    setTimeout(function timer() {
        console.log(j);
    }, j * 1000);
}

for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不只被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

for (let i = 0; i < 5; ++i) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

JS 相等

等于和不等于

ECMAScript的等于操作符使用两个等于号(==),不等于则使用(!=),这两个操作符都会先进行类型转换(强制类型转换)再确定操作数是否相等。

类型转换规则:

  1. 任一操作数为布尔值,先转换为数值再进行比较。false转换为0,true转换为1
  2. 一个操作数为字符串,另一个为数值,则将字符串转换为数值,再进行比较
  3. 一个操作数为对象,另一个不是,则调用对象的valueOf()方法取得原始值,再按照前面的规则进行比较

进行比较时遵循规则:

  1. nullundefined相等
  2. nullundefined不能转换为其他类型的值进行比较
  3. 任一操作数为NaN,相等操作符返回false,不等操作符返回true,因此按照规则,NaN不等于NaN
  4. 两个操作符都是对象,则比较他们是不是同一个对象,如果两个操作符指向同一个对象,则相等操作符返回true

全等与不全等

全等与不全等操作符在比较相等时不转换操作数。

null == undefined; // true
null === undefined; // false

示例:

内容相等的对象不相等, 不是同一个对象
两个不同对象的方法相等,其方法指向同一个对象,ECMAScript中函数是对象
相当于sayName每次都初始化一个新的对象,所以不相等
与上个例子等价

Angular 图片上传

input控件上传图片,div实现外观

//html
<div (click)="upload()" id="imagePreview">
    // 设置为自己的图片路径
    <img src="assets/increase.svg">
    <span id="text">上传图片</span>
</div>
<input type="file" id="file" accept="image/*" (change)="imagePreview($event)">
// css
#imagePreview {
    width: 80px;
    height: 80px;
    border: 1px dotted;
    padding-bottom: 5px;
    background-color: white;
    // 设置背景图片大小、不重复、居中
    background-repeat: no-repeat;
    background-position: center;
    background-size: 80px 80px;
}
input {
    display: none;
}
span {
    color: #707070;
}

效果:

添加交互效果

// js
upload() {
    document.getElementById('file').click(); // 触发上传
} 
// 预览图片
imagePreview(event) {
    const previewImage = document.getElementById('imagePreview');
    const uploadFile = document.getElementById('file');
    const reader = new FileReader();
    reader.onloadend = () => {
        // 设置图片为背景
        previewImage.style.backgroundImage = 'url(' + reader.result + ')';
        // 将原“+”号和文本内容隐藏
        const image = document.getElementById('image');
        image.style.display = 'none';
        const text = document.getElementById('text');
        text.style.display = 'none';
    };
    if (uploadFile) {
        reader.readAsDataURL(event.target.files[0]);
    }
}

上传效果: