JavaScript 逆向 ( 一 ) --- JavaScript 语法基础

JavaScript 基础

菜鸟教程 JavaScript 教程:https://www.runoob.com/js/js-tutorial.html

  • 1. 基础数据类型:number、string、boolean、null、undefined、object
  • 2. 顺序、条件、循环、比较
  • 3. 函数
  • 4. 运算符

强烈推荐书籍《JavaScript高级程序设计 第4版》:https://book.douban.com/subject/35175321/

示例:

详解 Javascript 中的 Object 对象

From:https://www.jb51.net/article/80177.htm

JS 中的 所有对象 都是 继承自 Object对象

创建 对象

"对象" 是一组相似数据和功能的集合,用它可以来模拟现实世界中的任何东西。

在 Javascript 中,创建对象的方式通常有两种方式

  • 构造函数。这种方式使用 new 关键字,接着跟上 Object 构造函数,再来给对象实例动态添加上不同的属性。这种方式相对来说比较繁琐,一般推荐使用对象字面量来创建对象。
    var person = new Object();
    person.name = "狼狼的蓝胖子";
    person.age = 25;
  • 对象字面量。对象字面量很好理解,使用 key/value 的形式直接创建对象,通过花括号将对象的属性包起来,对象的每个属性之间用逗号隔开。注意:如果是最后一个属性,后面就不要加逗号,因为在一些旧的浏览器下会报错。
    var person = {
      name: "狼狼的蓝胖子",
      age: 25
    };

注意:

obj_1 = {
	name: 'obj_1'
}

const {name} = obj_1;  // 相当于 name = obj_1.name
console.log(name)                 // obj
console.log(name === obj_1.name)  // true

对象实例属性方法

不管通过哪种方式创建了对象实例后,该 实例 都会拥有 下面的属性和方法,下面将会逐个说明。

constructor 属性

"constructor 属性" 是用来保存当前对象的构造函数的,前面的例子中,constructor 保存的就是 Object 方法。

var obj1 = new Object();
obj1.id = "obj1";
var obj2 = {
  "id": "obj2"
};
 
console.log(obj1.constructor);  //function Object(){}
console.log(obj2.constructor);  //function Object(){}
console.log(obj1.constructor === obj2.constructor)  // true

hasOwnProperty(propertyName) 方法

hasOwnProperty 方法接收一个字符串参数,该参数表示属性名称,用来判断该属性是否在当前对象实例中,而不是在对象的原型链中。我们来看看下面这个例子:

var arr = [];    
console.log(arr.hasOwnProperty("length"));          //true
console.log(arr.hasOwnProperty("hasOwnProperty"));  //false

在这个例子中,首先定义了一个数组对象的 实例arr,我们知道,数组对象实际是通过 原型链 ( 下面会介绍 ) 继承了Object 对象,然后拥有自己的一些属性,可以通过 hasOwnProperty 方法 判断 length 是 arr 自己的属性,然而 hasOwnProperty方法 是在 原型链 上的属性。

hasOwnProperty 方法 可以和 for..in 结合起来获取 对象自己的 key 

isPrototypeOf(Object) 方法

isPrototype 方法接收一个对象,用来判断当前对象是否在传入的参数对象的原型链上,说起来有点抽象,我们来看看代码。

function MyObject() {}
var obj = new MyObject();
console.log(Object.prototype.isPrototypeOf(obj));

上面代码中 MyObject 是继承自 Object 对象的,而在JS中,继承是通过 prototype 来实现的,所以 Object 的 prototype 必定在 MyObject 对象实例的原型链上。

propertyIsEnumerable(prototypeName) 方法

prototypeIsEnumerable 用来判断给定的属性是否可以被 for..in 语句给枚举出来。看下面代码:

var obj = {
    name: "objName"
}
for (var i in obj) {
    console.log(i);
}

执行这段代码输出字符串 “name”,这就说明通过 for…in 语句可以得到 obj 的 name 这个属性,但是我们知道,obj 的属性还有很多,比如 constructor,比如hasOwnPrototype 等等,但是它们没有被输出,说明这些属性不能被 for…in 给枚举出来,可以通过 propertyIsEnumerable 方法来得到。

console.log(obj.propertyIsEnumerable("constructor"));  // false

判断 “constructor” 是否可以被枚举,输出 false 说明无法被枚举出来。

toLocaleString() 方法

toLocalString 方法返回对象的字符串表示,和代码的执行环境有关。

var obj = {};
console.log(obj.toLocaleString());  //[object Object] 

var date = new Date();
console.log(date.toLocaleString());  //2021/4/15 下午1:30:15

toString() 方法

toString 用来返回对象的字符串表示。

var obj = {};
console.log(obj.toString());  //[object Object]
    
var date = new Date();
console.log(date.toString());  //Sun Feb 28 2021 13:40:36 GMT+0800 (中国标准时间)

valueOf() 方法

valueOf 方法返回对象的原始值,可能是字符串、数值 或 bool值 等,看具体的对象。

var obj = {
  name: "obj"
};
console.log(obj.valueOf());  //Object {name: "obj"}

var arr = [1];
console.log(arr.valueOf());  //[1]

var date = new Date();
console.log(date.valueOf());  //1456638436303

如代码所示,三个不同的对象实例调用 valueOf 返回不同的数据。

属性类型

在 Javascript 中,属性有两种类型,分别是

  • 数据 属性
  • 访问器 属性

我们来看看这两种属性具体是什么东西。

数据 属性

数据属性:可以理解为我们平时定义对象时赋予的属性,它可以进行读和写。但是,ES5中定义了一些 特性,这些特性是用来描述属性的各种特征。 特性的作用 是描述 属性的各种特征。特性是内部值,不能直接访问到。特性通过用两对方括号表示,比如[[Enumerable]]

属性特性会有一些默认值,要修改特性的默认值,必须使用 ES5 定义的新方法 Object.defineProperty 方法来修改

数据属性有4个描述其特征的特性,下面将依次说明每一个特性:

(1)[[Configurable]]:该特性表示是否可以通过 delete 操作符来删除属性,默认值是 true。

var obj = {};
obj.name = "myname";
    
delete obj.name;
console.log(obj.name);//undefined

这段代码很明显,通过 delete 删除了 obj 的 name 属性后,我们再访问 name 属性就访问不到了。

我们通过 Object.defineProperty 方法来修改 [[Configurable]] 特性。

var obj = {};
obj.name = "myname";
Object.defineProperty(obj, "name", {
  configurable: false
})        

delete obj.name;
console.log(obj.name);  //myname

通过将 configurable 特性设置成 false 之后,delete 就无法删除 name 属性了,如果在严格模式下,使用 delete 去删除就会报错。

(2)[[Enumerable]]:表示是否能够通过 for…in 语句来枚举出属性,默认是 true

我们来看看前面的例子:

var obj = {
    name: "objName"
}
for (var i in obj) {
    console.log(i);//name
}

这段代码只输出了 name 属性,我们来将 constructor 属性的 [[Enumerable]] 设置为 true 试试。

var obj = {
    name: "objName"
}
Object.defineProperty(obj, "constructor", {
    enumerable: true
})

for (var i in obj) {
    console.log(i);//name,constructor
}
console.log(obj.propertyIsEnumerable("constructor"));//true

这段代码中,for…in 循环得到了 name 和 constructor 两个属性,而通过 propertyIsEnumerable 方法来判断 constructor 也返回了true。

(3)[[Writable]]:表示属性值是否可以修改,默认为true。如果 [[Writable]] 被设置成 false,尝试修改时将没有效果,在严格模式下会报错

(4)[[Value]]:表示属性的值,默认为 undefined

我们通过一个简单的例子来看看这两个特性:

var obj = {
    name: "name"
};
console.log(obj.name);//name    

Object.defineProperty(obj, "name", {
    value: "newValue",
    writable: false
})
console.log(obj.name);//newValue

obj.name = "oldValue";
console.log(obj.name);//newValue

我们首先定义了 obj 对象的 name 属性值为 “name”,然后通过 defineProperty 方法来修改值,并且将其设置为不可修改的。接着我们再修改 name 属性的值,可以发现修改无效。

如果我们通过 defineProperty 来修改 name 属性的值,是否可以修改呢?答案是可以的:

Object.defineProperty(obj, "name", {
  value: "oldValue"
})
console.log(obj.name); //oldValue

访问器 属性

访问器属性有点类似于 C# 中的属性,和数据属性的区别在于,它没有数据属性的 [[Writable]] 和 [[Value]] 两个特性,而是拥有一对 getter 和 setter 函数。

  • [[Get]]:读取属性时调用的函数,默认是 undefined
  • [[Set]]:设置属性时调用的函数,默认是 undefined

getter 和 setter 是一个很有用的东西,假设有两个属性,其中第二个属性值会随着第一个属性值的变化而变化。这种场景在我们平时的编码中起始是非常常见的。在之前的做法中,我们往往要去手动修改第二个属性的值,那现在我们就可以通过 get 和 set 函数来解决这个问题。看下面这个例子:

var person = {
    age: 10
}

Object.defineProperty(person, "type", {
    get: function () {
        if (person.age > 17) {
            return "成人";
        }
        return "小孩";
    }
})

console.log(person.type);//小孩

person.age = 18;
console.log(person.type);//成人

通过修改 age 的值,type 的值也会相应的修改,这样我们就不用再手动的去修改 type 的值了。

下面这种方式也是可以实现同样的效果:

var person = {
    _age: 10,
    type: "小孩"
}

Object.defineProperty(person, "age", {
    get: function () {
        return this._age;
    },
    set: function (newValue) {
        this._age = newValue;
        this.type = newValue > 17 ? "成人" : "小孩";
    }
})
console.log(person.type);

person.age = 18;
console.log(person.type);

访问器 属性 的 注意点

关于访问器属性,有几点要注意:

  • 1、严格模式下,必须同时设置 get 和 set
  • 2、非严格模式下,可以只设置其中一个,如果只设置 get,则属性是只读的,如果只设置 set,属性则无法读取
  • 3、Object.defineProperty 是 ES5 中的新方法,IE9(IE8部分实现,只有dom对象才支持)以下浏览器不支持,一些旧的浏览器可以通过非标准方法defineGetter()和defineSetter()来设置,这里就不说明了,有兴趣的同学可以查找相关资料。

特性 操作的相关方法

ES5 提供了一些读取或操作属性特性的方法,前面用到的 Object.defineProperty 就是其中之一。我总结了一些比较常用的方法如下:

(1)Object.defineProperty

定义一个对象的属性,这个方法前面我们已经用到多次,简单说说其用法。

Object.defineProperty(obj, propName, descriptor);

defineProperty 有点类似于定于在 Object 上的静态方法,通过 Object 直接调用,它接收3个参数:

  • obj:需要定义属性的对象
  • propNane:需要被定义的属性名称
  • defineProperty:属性描述符,包含一些属性的特性定义

例子如下:

var obj = {};
Object.defineProperty(obj, "name", {
  value: "name",
  configurable: true,
  writable: true,
  enumerable: true
});

(2)Object.defineProperties

和 defineProperty 类似,是用来定义对象属性的,不同的是它可以用来同时定义多个属性,我们通过命名也可以看出来,用法如下:

var obj = {};
Object.defineProperty(obj, {
    "name": {
        value: "name",
        configurable: true,
        writable: true,
        enumerable: true
    },
    "age": {
        value: 20
    }
});

(3)Object.getOwnPropertyDescriptor

ES5 中还提供了一个读取特性值的方法,该方法接收对象及其属性名作为两个参数,返回一个对象,根据属性类型的不同,返回对象会包含不同的值。

var person = {
    _age: 10,
    type: "小孩"
}
Object.defineProperty(person, "age", {
    get: function () {
        return this._age;
    },
    set: function (newValue) {
        this._age = newValue;
        this.type = newValue > 17 ? "成人" : "小孩";
    }
})

console.log(Object.getOwnPropertyDescriptor(person, "type"));//Object {value: "成人", writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(person, "age")); //Object {enumerable: false, configurable: false, get: function(),set: function ()}

Object方法

在 ES5 中,Object 对象上新增了一批方法,这些方法可以直接通过 Object 进行访问,前面用到的 defineProperty 就是新增的方法之一。除此之外还有很多方法,我将其总结归纳如下:

对象创建型方法 Object.create(proto, [propertiesObject])

在前面我们提到,创建一个对象有两种方法:构造函数对象字面量

这两种方法有一个缺点就是:如果要创建多个对象,写起来很繁琐,所以后来就有了一种创建自定义构造函数的方法来创建对象,如下所示:

function Person(name, age) {
    this.name = name;
    this.age = age;
}
var person = new Person("Jack", 15);

这种方式可以很方便的创建多个同样的对象,也是目前比较常用的方法。

ES5 提供的 Object.create 方法也是一个创建对象的方法,这个方法允许为创建的对象选择原型对象,不需要定义一个构造函数。用法如下:

var obj = Object.create(Object.prototype, {
    name: {
        value: "Jack"
    }
})
console.log(obj.name); //Jack

这个方法接收的第一个参数作为被创建对象的原型,第二个参数是对象的属性。

注意:在这个例子中,name属性是无法被修改的,因为它没有设置writable特性,默认则为false。

个人看法:Object.create这种创建对象的方式略显繁琐,除非是需要修改属性的特性,否则不建议使用这种方式创建对象。

获取 属性 

Object.keys  获取自身属性,但不包括原型中的属性

Object.keys 是 es5 中新增的方法用来获取对象自身所有的可枚举的属性名,但不包括原型中的属性然后返回一个由属性名组成的数组。

注意它同 for..in 一样不能保证属性按对象原来的顺序输出。

function Parent() {
    this.lastName = "Black"
}

function Child(firstName) {
    this.firstName = firstName;
}

Child.prototype = new Parent();

var son = new Child("Jack");
console.log(Object.keys(son)); //["firstName"]

代码中返回了 firstName,并没有返回从 prototype 继承而来的 lastName 不可枚举的相关属性

// simple array
var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']

// array like object
var obj = {0: 'a', 1: 'b', 2: 'c'};
console.log(Object.keys(obj)); // console: ['0', '1', '2']

// array like object with random key ordering
var anObj = {100: 'a', 2: 'b', 7: 'c'};
console.log(Object.keys(anObj)); // console: ['2', '7', '100']

// getFoo is a property which isn't enumerable
var myObj = Object.create({}, {
    getFoo: {
        value: function () {
            return this.foo;
        }
    }
});
myObj.foo = 1;
console.log(Object.keys(myObj)); // console: ['foo']

在一些旧的浏览器中,我们可以使用 hasOwnProperty for…in 来达到类似的效果。

function Parent() {
    this.lastName = "Black"
}

function Child(firstName) {
    this.firstName = firstName;
}

Child.prototype = new Parent();

var son = new Child("Jack");

// 注意:这里如果不支持 Object.keys,则把 Object.keys 定义为一个函数
Object.keys = Object.keys ||
    function (obj) {
        var keys = [];
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                keys.push(key);
            }
        }
        return keys;
    };

console.log(Object.keys(son)); 

getOwnPropertyNames  获取自身 可枚举不可枚举 属性

Object.getOwnPropertyNames 也是 es5 中新增的方法,返回对象自身的所有属性的属性名包括可枚举和不可枚举的所有属性)组成的数组,但不会获取原型链上的属性

function Parent() {
    this.lastName = "Black"
}

function Child(firstName) {
    this.firstName = firstName;
}

Child.prototype = new Parent();

var son = new Child("Jack");
Object.defineProperty(son, "age", {
    enumerable: false
})
console.log(Object.keys(son));//["firstName"] 
console.log(Object.getOwnPropertyNames(son));//["firstName", "age"]

我们定义给 son 对象定义了一个不可枚举的属性 age,然后通过 keysgetOwnPropertyNames 两个方法来获取属性列表,能明显看出了两者区别。

属性 特性型 方法

这个主要是前面提到的三个方法:

  • defineProperty
  • defineProperties
  • getOwnPropertyDescriptor 

对象 限制型 方法

ES5 中提供了一系列限制对象被修改的方法,用来防止被某些对象被无意间修改导致的错误。每种限制类型包含一个判断方法和一个设置方法。

阻止对象扩展

Object.preventExtensions()  用来限制对象的扩展,设置之后,对象将无法添加新属性,用法如下:

Object.preventExtensions(obj);

该方法接收一个要被设置成无法扩展的对象作为参数,需要注意两点:

  • 1、对象的属性不可用扩展,但是已存在的属性可以被删除
  • 2、无法添加新属性指的是无法在自身上添加属性,如果是在对象的原型上,还是可以添加属性的。
function Person(name) {
    this.name = name;
}

var person = new Person("Jack");
Object.preventExtensions(person);

delete person.name;
console.log(person.name);//undefined

Person.prototype.age = 15;
console.log(person.age);//15

Object.isExtensible 方法用来判断一个对象是否可扩展,默认情况是 true

将对象密封

Object.seal 可以密封一个对象并返回被密封的对象。

密封对象无法添加或删除已有属性,也无法修改属性的 enumerable,writable,configurable,但是可以修改属性值。

function Person(name) {
    this.name = name;
}

var person = new Person("Jack");
Object.seal(person);
delete person.name;
console.log(person.name);  //Jack

将对象密封后,使用 delete 删除对象属性,还是可以访问得到属性。

通过 Object.isSealed 可以用来判断一个对象是否被密封了。

冻结对象

Object.freeze 方法用来冻结一个对象,被冻结的对象将无法添加,修改,删除属性值,也无法修改属性的特性值,即这个对象无法被修改。

function Person(name) {
    this.name = name;
}

var person = new Person("Jack");
Object.freeze(person);

delete person.name;
console.log(person.name);//Jack

Person.prototype.age = 15;
console.log(person.age);//15

分析上面的代码我们可以发现,被冻结的对象无法删除自身的属性,但是通过其原型对象还是可以新增属性的。

通过Object.isFrozen可以用来判断一个对象是否被冻结了。

可以发现:这三个限制对象的方法的限制程度是依次上升的。

Javascript Object 常用方法总结

参考:https://www.cnblogs.com/fozero/p/10997520.html

Object.keys(ojb) 方法

Object.keys(obj) 方法是 JavaScript 中用于遍历对象属性的一个方法 。它传入的参数是一个对象,返回的是一个数组,数组中包含的是该对象所有的属性名。如:

var cat = {
    name: 'mini',
    age: 2,
    color: 'yellow',
    desc:"cute"
}
console.log(Object.keys(cat)); // ["name", "age", "color", "desc"]

这里有一道关于 Object.keys 的题目:输出对象中值大于 2的 key 的数组

var data = {a: 1, b: 2, c: 3, d: 4};
Object.keys(data).filter(function (x) {
    return 1;
})

/*
期待输出:["c","d"]
请问1处填什么?
正确答案:1 :data[x]>2
*/

Object.keys 是 es5 中新增的方法,用来获取对象自身所有的可枚举的属性名,但不包括原型中的属性然后返回一个由属性名组成的数组注意它同 for..in 一样不能保证属性按对象原来的顺序输出。
Object.getOwnPropertyNames 也是 es5 中新增的方法,返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性

Array.filter(function)

对数组进行过滤返回符合条件的数组。

Object.values() 方法

Object.values 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历( enumerable )属性的键值。

var obj = {foo: "bar", baz: 42};
Object.values(obj)
// ["bar", 42]  

返回数组的成员顺序,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a。Object.values 只返回对象自身的可遍历属性。

var obj = {100: 'a', 2: 'b', 7: 'c'};
Object.values(obj)
// ["b", "c", "a"]  

如果 Object.values 方法的参数是一个字符串,会返回各个字符组成的一个数组。

Object.values('foo')
// ['f', 'o', 'o']  

上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组。
如果参数不是对象,Object.values 会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values 会返回空数组。

Object.create()

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

语法Object.create(proto, [propertiesObject])
参数
        :proto  新创建对象的原型对象。
        :propertiesObject  可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。

返回值:一个新对象,带着指定的原型对象和属性。如:

var parent = {
    x: 1,
    y: 1
}
var child = Object.create(parent, {
    z: {                           // z会成为创建对象的属性
        writable: true,
        configurable: true,
        value: "newAdd"
    }
});
console.log(child)//{z: "newAdd"}z: "newAdd"__proto__: x: 1y: 1__proto__: Object

Object.create()  创建继承

function A() {
    this.a = 1;
    this.b = 2;
}

A.prototype.drive = function () {
    console.log('drivvvvvvvvvv');
}

//方式1
function B() {
}

B.prototype = Object.create(new A()); //这里采用了new 一个实例
//方式2
function C() {
    A.call(this);
}

C.prototype = Object.create(A.prototype) //这里使用的是父类的原型

以上两种方式有什么区别?
1 的缺点:
        执行了 new,相当于运行了一遍 A ,如果在 A 里做了一些其它事情(如改变全局变量)就会有副作用。
        用 A 创建的对象做原型,里面可能会有一些冗余的属性。
2 模拟了 new 的执行过程

Object.hasOwnProperty() 方法

判断对象自身属性中是否具有指定的属性。这个方法是不包括对象原型链上的方法的。

判断某个对象是否拥有某个属性,判断的方法有很多,常用的方法就是 object.hasOwnProperty('×××')

var obj = {
    name: 'fei'
}
console.log(obj.hasOwnProperty('name'))      // true
console.log(obj.hasOwnProperty('toString'))  // false

以上,obj 对象存在的 name 属性的时候,调用这个方法才是返回 true,我们知道其实每个对象实例的原型链上存在 toString 方法,在这里打印 false,说明这个方法只是表明实例对象的属性,不包括原型链上的属性。

Object.getOwnPropertyNames() 方法

Object.getOwnPropertyNames() 方法返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性

function A(a, aa) {
    this.a = a;
    this.aa = aa;
    this.getA = function () {
        return this.a;
    }
}

// 原型方法
A.prototype.aaa = function () {
};

var B = new A('b', 'bb');
B.myMethodA = function () {
};
// 不可枚举方法
Object.defineProperty(B, 'myMethodB', {
    enumerable: false,
    value: function () {
    }
});

Object.getOwnPropertyNames(B); // ["a", "aa", "getA", "myMethodA", "myMethodB"]

Object.getOwnPropertyNamesObject.keys 区别

Object.getOwnPropertyNamesObject.keys 的区别,

  • Object.keys 只适用于可枚举的属性,
  • Object.getOwnPropertyNames 返回对象自动的全部属性名称。
'use strict';
(function () {
    if (!Object.getOwnPropertyNames) {
        console.log('浏览器不支持getOwnPropertyNames');
        return;
    }

    //人类的构造函数
    var person = function (name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;

        this.sing = function () {
            console.log('sing');
        }
    }
    //new 一个ladygaga
    var gaga = new person('ladygaga', 26, 'girl');

    //给嘎嘎发放一个不可枚举的身份证
    Object.defineProperty(gaga, 'id', {
        value: '1234567890',
        enumerable: false
    });

    //查看gaga的个人信息
    var arr = Object.getOwnPropertyNames(gaga);
    document.write(arr); //output: name,age,sex,sing,id

    document.write('</br>');

    //注意和getOwnPropertyNames的区别,不可枚举的id没有输出
    var arr1 = Object.keys(gaga);
    document.write(arr1); //output: name,age,sex,sing
})();

es6 中 JavaScript 对象方法 Object.assign()

Object.assign 方法用于对象的合并,将源对象( source )的所有可枚举属性,复制到目标对象( target )。

var target = {a: 1};
var source1 = {b: 2};
var source2 = {c: 3};
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

1、如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
2、如果只有一个参数,Object.assign 会直接返回该参数。

var obj = {a: 1};  
Object.assign(obj) === obj // true  

3、如果该参数不是对象,则会先转成对象,然后返回。
4、由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
5、Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

var obj1 = {a: {b: 1}};  
var obj2 = Object.assign({}, obj1);  
obj1.a.b = 2;  
obj2.a.b // 2  

上面代码中,源对象obj1的a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

常见用途

( 1 )为对象添加属性

class Point {
    constructor(x, y) {
        Object.assign(this, {x, y});
    }
}  

上面方法通过Object.assign方法,将x属性和y属性添加到Point类的对象实例。

( 2 )为对象添加方法

Object.assign(SomeClass.prototype, {
    someMethod(arg1, arg2) {
        //···
    },
    anotherMethod() {
        //···
    }
});
//  等同于下面的写法  
SomeClass.prototype.someMethod = function (arg1, arg2) {
    //···
};
SomeClass.prototype.anotherMethod = function () {
    //···
};  

上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用 assign 方法添加到 SomeClass.prototype 之中。

( 3 )克隆对象

function clone(origin) {  
    return Object.assign({}, origin);  
}  

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。

( 4 )合并多个对象

将多个对象合并到某个对象。

const merge =(target, ...sources) => Object.assign(target, ...sources);  

如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

const merge =(...sources) => Object.assign({}, ...sources);  

( 5 )为属性 指定 默认值

const DEFAULTS = {
    logLevel: 0,
    outputFormat: 'html'
};

function processContent(options) {
    let options = Object.assign({}, DEFAULTS, options);
}  

DEFAULTS 对象是默认值,options 对象是用户提供的参数。Object.assign 方法将 DEFAULTS 和 options 合并成一个新对象,如果两者有同名属性,则 option 的属性值会覆盖 DEFAULTS 的属性值。注意,由于存在深拷贝的问题,DEFAULTS 对象 和 options对象的所有属性的值,都只能是简单类型,而不能指向另一个对象。否则,将导致DEFAULTS 对象的该属性不起作用。

参考 es6 javascript 对象方法Object.assign():https://blog.csdn.net/qq_30100043/article/details/53422657

Object.defineProperty() 方法理解

Object.defineProperty 可以用来定义新属性或修改原有的属性

使用构造函数定义对象和属性

var obj = new Object; //obj = {}
obj.name = "张三"; //添加描述
obj.say = function(){}; //添加行为

语法:Object.defineProperty(obj, prop, descriptor)

参数说明

  • obj:必需。目标对象
  • prop:必需。需定义或修改的属性的名字
  • descriptor:必需。目标属性所拥有的特性

给对象的属性添加特性描述,目前提供两种形式:

  • 数据描述
  • 存取器描述

数据 描述

修改或定义对象的某个属性的时候,给这个属性添加一些特性, 数据描述中的属性都是可选的

var obj = {
    test:"hello"
}

//对象已有的属性添加特性描述
/*
Object.defineProperty(obj,"test",{
    configurable:true | false,
    enumerable:true | false,
    value:任意类型的值,
    writable:true | false
});
*/

//对象新添加的属性的特性描述
/*
Object.defineProperty(obj,"newKey",{
    configurable:true | false,
    enumerable:true | false,
    value:任意类型的值,
    writable:true | false
});
*/
  • value: 设置属性的值
  • writable: 值是否可以重写。true | false
  • enumerable: 目标属性是否可以被枚举。true | false
  • configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false

存取器 描述

使用存取器描述属性的特性的时候,允许设置以下特性属性, 当使用了getter 或 setter 方法,不允许使用 writable 和 value 这两个属性

var obj = {};
Object.defineProperty(obj, "newKey", {
    get: function () {} | undefined,
    set: function (value) {} | undefined,
    configurable: true | false,
    enumerable: true | false
});

getter / setter

getter 是一种获得属性值的方法。setter是一种设置属性值的方法。使用 get/set 属性来定义对应的方法

var obj = {};
var initValue = 'hello';
Object.defineProperty(obj, "newKey", {
    get: function () {
        //当获取值的时候触发的函数
        return initValue;
    },
    set: function (value) {
        //当设置值的时候触发的函数,设置的新值通过参数value拿到
        initValue = value;
    }
});

//获取值console.log(obj.newKey); //hello
//设置值
obj.newKey = 'change value';
console.log(obj.newKey); //change value

原型链

  • 1. 每个函数( 函数也是对象 )都有 prototype __proto__
  • 2. 每一个对象 / 构造函数的实例这个也是对象都有 __proto__
  • 3. 实例 的  __proto__  指向 构造函数 的 prototype。这个称为 构造函数的原型对象
  • 4. JavaScript 引擎会沿着 __proto__  --->  ptototype 的顺序一直往上方查找,找到 window.Object.prototype 为止,Object 为原生底层对象,到这里就停止了查找。
          如果没有找到,就会报错或者返回 undefined
  • 5. 而构造函数的 __proto__  指向 Function.prototype  ƒ () { [native code] }构造器函数但这个叫法  并不准确,它目前没有一个合适的中文名
  • 6. __proto__ 是浏览器厂商实现的,W3C规范中并没有这个东西

  • 1. JS 代码还没运行的时候,JS 环境里已经有一个 window 对象了。
  • 2. window 对象有一个 Object 属性,window.Object 是一个 函数对象
  • 3. window.Object 这个函数对象有一个重要属性是 prototype
  • 4. window.Object.prototype 里面有一堆属性
  • 5. 所有的实例函数的 __proto__ 都会指向 构造函数的 prototype
  • 6. constructor 反向的prototype

var obj = {};
obj.toString();

上面定义了一个 空对象obj,当调用 obj 的 toString() 理论上会报 undefined 错误,但是实际上不会报错

运算符 -- JavaScript 标准参考教程(alpha):http://javascript.ruanyifeng.com/grammar/operator.html#toc6

JavaScript 中 ===== 的区别 ( 参考:https://www.zhihu.com/question/31442029 ):

  • "==="  叫做 恒等 运算符。类型 都相等 )( 其实叫 全等运算符 更合适。即内存中每个bit位都一样 )
        严格相等运算符 === 的运算规则如下:
        (1) 不同类型值。如果两个值的类型不同,直接返回false。
        (2) 同一类的原始类型值。同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true,值不同就返回false。
        (3) 同一类的复合类型值。两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象。
        (4) undefined 和 null。undefined 和 null 与自身严格相等。
            null === null  //true
            undefined === undefined  //true
  • "=="  叫做 相等 运算符。( 只 判断数据的值
        相等运算符 == 在比较相同类型的数据时,与严格相等运算符完全一样。
        在比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。
        类型转换规则如下:
        (1) 原始类型的值。原始类型的数据会转换成数值类型再进行比较。字符串和布尔值都会转换成数值。
        (2) 对象与原始类型值比较。对象(这里指广义的对象,包括数值和函数)与原始类型的值比较时,对象转化成原始类型的值,再进行比较。
        (3) undefined和null。undefined和null与其他类型的值比较时,结果都为false,它们互相比较时结果为true。
        (4) 相等运算符的缺点。相等运算符会隐藏类型的转换,然后带来一些违反直觉的结果。
  • 因为"=="不严谨,可能会带来一些违反直觉的后果,建议尽量不要使用 相等运算符 == ,而是使用 严格相等运算符 === 

示例:

'' == '0'  // false
0 == ''    // true。'' 会先转换成数值0,再和0比较,所以是 true
0 === 0    // true

false == 'false'  // false
false == 0        // true

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

'\t\r\n' == 0

var a = undefined;
if(!a){
    console.log('a'); // 1
}

if(a == null){
	console.log('a');  // 1
}

if( a === null ){
	console.log('a');  // 无输出
}

js中 !== != 的区别:

  • !=    会转换成相同类型 进行比较。即 在表达式两边的数据类型不一致时,会隐式转换为相同数据类型,然后对值进行比较;
  • !==   不会进行类型转换,在比较时除了对值进行比较以外,还比较两边的数据类型, 它是 恒等运算符 === 的非形式。

解释:上面是定义 obj 变量指向一个空对象,当对象定义的一瞬间,就会瞬间产生一个 __proto__ 的属性 ,这个属性指向 window.Object.prototype。

上面这个搜索的过程是由 __proto__ 组成的链子一直走下去的,这个过程就叫做 原型链

上面是一个 "链",下面继续深入,看下数组

var arr = []
arr.push(1) // [1]

再复杂一下,arr.valueOf() 做了什么?

  • arr 自身没有 valueOf,于是去 arr.__proto__ 上找
  • arr.__proto__ 只有 pop、push 也没有 valueOf,于是去 arr.__proto__.__proto__ 上找
  • arr.__proto__.__proto__ 就是 window.Object.prototype
  • 所以 arr.valueOf 其实就是 window.Object.prototype.valueOf
  • arr.valueOf() 等价于 arr.valueOf.call(arr)
  • arr.valueOf.call(arr) 等价于 window.Object.prototype.valueOf.call(arr)

函数进阶

JS中一切皆对象对象是拥有属性和方法的数据JS函数也是对象

当创建一个函数的时候,发生了什么?

实际上,函数 是 Function类型实例此时可以把每一个创建出来的函数,当成是Function类型的实例对象

所以函数本身拥有的对象属性是来源于 Function,Fn.Constructor 即为 Function

但是与此同时要注意:Function.prototype.__proto__ === Object.prototype

可以理解为:构造器函数的构造函数是Object

也可以简单的理解:函数即对象

如果上面看不懂,可以继续看下面就会明白。。。

构造函数

每个 函数 都有一个 原型对象(prototype)

原型对象 都包含一个指向 构造函数 的 指针, 
实例(instance) 都包含一个指向 原型对象 的 内部指针。

1. 在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。构造函数首字母一般大写。

示例:使用构造函数创建一个对象,在这个例子中,Person 就是一个构造函数,然后使用 new 创建了一个 实例对象 person

function Person() {
}
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin

示例:

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function() {
    alert(this.name)
  }
}
var person1 = new Person('Zaxlct', 28, 'Engineer')
var person2 = new Person('Mick', 23, 'Doctor')

person1 和 person2 都是 Person 的实例。这两个实例都有一个 constructor (构造函数)属性,该属性(是一个指针)指向 Person。 即:

console.log(person1.constructor == Person) //true
console.log(person2.constructor == Person) //true

2.构造函数的执行过程

function Person(name, sex, age){
    this.name = name;
    this.sex = sex;
    this.age = age;
}
p1 = new Person('king', '男', 100);
  • (1)  当以 new 关键字调用时,会创建一个新的内存空间,标记为 Person 的实例
  • (2)  函数体内部的 this 指向该内存,每当创建一个实例的时候,就会创建一个新的内存空间
  • (3)  给 this 添加属性,就相当于给实例添加属性
  • (4)  由于函数体内部的 this 指向新创建的内存空间,默认返回 this ,就相当于默认返回了该内存空间
function Cat(){
    this.cat = 'cat';   
}
guaiguai = new Cat();  // guaiguai 是 Cat 的实例化对象
guaiguai.__proto__     // 因为guaiguai是Cat的实例化对象,
                       // 所以 __proto__ 指向的是 构造函数的 prototype
					   // 这个就叫 构造函数的原型对象
guaiguai.__proto__ === Cat.prototype  // true

prototype

每个 函数 都有一个 prototype 属性,就是我们经常在各种例子中看到的那个 prototype ,指向调用该构造函数而创建的 实例的原型

比如:上面例子中的 person1 和 person2 的原型

function Person() {}
Person.prototype.name = 'Zaxlct'
Person.prototype.age = 28
Person.prototype.job = 'Engineer'
Person.prototype.sayName = function() {
  alert(this.name)
}

var person1 = new Person()
person1.sayName() // 'Zaxlct'

var person2 = new Person()
person2.sayName() // 'Zaxlct'

console.log(person1.sayName == person2.sayName) //true

示例:

function Person() {
}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin

那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?

其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数 而创建 的 实例的原型,也就是这个例子中的 person1 和 person2 的原型。

那什么是原型呢 ?

可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。

让我们用一张图表示构造函数和实例原型之间的关系:

在这张图中我们用 Object.prototype 表示实例原型。

那么我们该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性:__proto__

JS 的继承是用过原型链实现的

给构造函数添加属性

var Person = function(a){	
	this.a = a;
	return this.a;
}

person_1 = new Person()

Person.prototype.return666 = function(){
	return 666;
}

person_2 = new Person();
console.log(person_1.return666);

__proto__

这是每一个JavaScript对象(除了 null )都具有的一个属性,叫 __proto__,这个属性会指向该对象的原型。

为了证明这一点,我们可以在火狐或者谷歌中输入:

function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

于是我们更新下关系图:

既然 实例对象构造函数 都可以指向原型,那么 原型 是否有属性指向 构造函数或者实例 呢?

constructor

  • 原型 指向 实例 倒是没有,因为一个构造函数可以生成多个实例。
  • 但是 原型 指向 构造函数 倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。

为了验证这一点,我们可以尝试:

function Person() {}
var person1 = new Person()
console.log(Person === Person.prototype.constructor) // true
console.log(person1.__proto__ === Person.prototype) // true

所以再更新下关系图:

function a(){}
a.constructor === Function.constructor  // true

构造函数constructor 指向 构造函数自身

综上我们已经得出:

function Person() {
}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲 实例原型 的关系:

实例原型

当读取 实例的属性 时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

举个例子:

function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin

在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性,就会从 person 的原型也就是 person.__proto__ ,也就是 Person.prototype 中查找,幸运的是我们找到了 name 属性,结果为 Kevin。

但是万一还没有找到呢?原型的原型又是什么呢?

原型原型

在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin

所以原型对象是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__  指向 构造函数的 prototype ,所以我们再更新下关系图:

原型链

那 Object.prototype 的原型呢?

null,不信我们可以打印:console.log(Object.prototype.__proto__ === null) // true

所以查到属性的时候查到 Object.prototype 就可以停止查找了。所以最后一张关系图就是

顺便还要说一下,图中由 相互关联的原型组成的链状结构 就是 原型链也就是蓝色的这条线。

补充

最后,补充三点大家可能不会注意的地方:

constructor

首先是 constructor 属性,我们看个例子:

function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true

当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到 constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:

person.constructor === Person.prototype.constructor

__proto__

其次是 __proto__ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。

真的是继承吗?

最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承'属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:

继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

自执行 匿名 函数

  • 声明式函数。会导致函数提升,function会被解释器优先编译。即我们用声明式写函数,可以在任何区域声明,不会影响我们调用。function XXX(){}
            fn1();
            function fn1(){}    // 可以正常调用
  • 函数表达式。函数表达式经常使用,而函数表达式中的 function 则不会出现函数提升。而是JS解释器逐行解释,到了这一句才会解释。因此如果调用在函数表达式之前,则会调用失败。var k = function(){}
            fn2();
            var fn2 = function(){}    // 无法调用

小括号 的 作用:

  • 小括号 能把表达式组合分块,并且每一块,也就是每一对小括号,都有一个返回值。返回值实际上也就是 小括号中 表达式的返回值
  • 所以,当我们用一对小括号把匿名函数括起来的时候,实际上 小括号对 返回的就是一个匿名函数的 Function 对象。
  • 因此,小括号对 加上 匿名函数 就如同 有名字的函数 被我们取得它的引用位置了。所以如果在这个引用变量后面再加上参数列表,就会实现普通函数的调用形式。
  • 简单来说就是:小括号有返回值,也就是小括号内的函数或者表达式的返回值,所以说小括号内的 function 返回值等于小括号的返回值

自执行函数

1. 先来看个最简单的自执行函数

(function(){

}());

相当于声明并调用

var b = function () {
   
}
b()

2. 自执行函数也可以有名字

function b(){
    ...
}()

3. 执行函数也可以传参

function b(i){
    console.log(i)
}(5)

总结:自执行函数 在调用上与普通函数一样,可以匿名,可以传参。只不过是在声明的时候自调用了一次

常见的自执行匿名函数:

  • 第一种:(function(x,y){return x+y;})(3,4);              // 两个()() ,function写在第一个()里面,第二个()用来传参,这种比较常见。默认返回值 是  undefine
  • 第二种:(function(param) { alert(param);})('张三');       // 自执行函数的传参。默认返回值 是  undefine
  • 第三种:(function(param) { console.log(param); return arguments.callee;})('html5')('php');   

不常见的自执行匿名函数:

  • 第四种:~(function(){ alert('hellow world!'); })();        // 默认返回值 是 -1
    ~function(x, y) {
        return x+y;
    }(3, 4); 
  • 第五种:void function(){ alert('hellow world!'); }();     // 默认返回值 是  undefine。  void function(x) {x = x-1;}(9);
  • 第六种:+function(){ alert('hellow world!'); }();          // 默认返回值 是 NaN
    +function(x,y){
        return x+y;
    }(3,4);
    
    ++function(x,y){
        return x+y;
    }(3,4); 
  • 第七种:-function(){ alert('hellow world!'); }();           // 默认返回值 是 NaN
    -function(x,y){
        return x+y;
    }(3,4);
    
    --function(x,y){
        return x+y;
    }(3,4);
  • 第八种:!function(){ alert('hellow world!'); }();           // 默认返回 true,是一个 bool 值
  • 第九种:(  function(x,y){return x+y;}(3,4)  );          // 一个() ,里面写 function(){}()  默认返回值 是  undefine
  • 匿名函数的执行放在 [ ] 中:
    [function(){
        console.log(this)  // 浏览器的控制台输出window
    }(this)] 
  • 匿名函数前加 typeof

    typeof function(){
        console.log(this) // 浏览器得控制台输出window
    }(this) 

示例:匿名函数自执行 实现 异步函数递归

(function async(i) {    
    if (i >= 5){ return }
    else{
		setTimeout(() => {
            console.log(i)
            i++
            async(i)
        }, 1000)
	}                    
})(0)

自执行匿名函数 执行耗时结果:

  • new 方法永远最慢。
  • 括号 在测试里表现始终很快,在大多数情况下比感叹号更快,甚至可以说是最优的。
  • 加减号 在chrome表现惊人,而且在其他浏览器下也普遍很快,相比加号效果更好。当然这只是个简单测试,不能说明问题。

但有些结论是有意义的:括号和加减号最优

面向对象 --- 封装

封装:把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口。

在ES6之前,是不存在 class 这个语法糖类的。所以实现大多采用原型对象和构造函数

  • 私有 属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var声明的属性)
  • 公有 属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
  • 静态 属性和方法:定义在构造函数上的方法(比如Cat.xxx),不需要实例就可以调用(例如Object.assign())

在ES6之后,存在class这个语法糖类。当你使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回,因此它被称为constructor构造方法(函数)。

私有变量、函数

在函数内部定义的变量和函数如果不对外提供接口,那么外部将无法访问到,也就是变为私有变量和私有函数。

function Obj() {
  var a = 0 //私有变量
  var fn = function() {
    //私有函数
  }
}

var o = new Obj()
console.log(o.a) //undefined
console.log(o.fn) //undefined

静态变量、函数

当定义一个函数后通过 “.”为其添加的属性和函数,通过对象本身仍然可以访问得到,但是其实例却访问不到,这样的变量和函数分别被称为静态变量和静态函数。

function Obj() {}
  Obj.a = 0 //静态变量
  Obj.fn = function() {
    //静态函数
}

console.log(Obj.a) //0
console.log(typeof Obj.fn) //function

var o = new Obj()
console.log(o.a) //undefined
console.log(typeof o.fn) //undefine

实例变量、函数

在面向对象编程中除了一些库函数我们还是希望在对象定义的时候同时定义一些属性和方法,实例化后可以访问,JavaScript也能做到这样。

function Obj(){
    this.a=[]; //实例变量
    this.fn=function(){ //实例方法    
    }
}
 
console.log(typeof Obj.a); //undefined
console.log(typeof Obj.fn); //undefined
 
var o=new Obj();
console.log(typeof o.a); //object
console.log(typeof o.fn); //function

测试

function Foo() {
  getName = function() {
    alert(1)
  }
  return this
}
Foo.getName = function() {
  alert(2)
}
Foo.prototype.getName = function() {
  alert(3)
}
var getName = function() {
  alert(4)
}
function getName() {
  alert(5)
}

请写出以下输出结果:

Foo.getName()
getName()
Foo().getName()
getName()
new Foo.getName()
new Foo().getName()
new new Foo().getName()

解读:首先定义了一个叫 Foo 的函数,之后为 Foo 创建了一个叫 getName 的静态属性存储了一个匿名函数,之后为 Foo 的原型对象新创建了一个叫 getName 的匿名函数。之后又通过函数变量表达式创建了一个 getName 的函数,最后再声明一个叫 getName 函数。

先来剧透一下答案,再来看看具体分析

//答案:
Foo.getName()       // 2
getName()           // 4
Foo().getName()     // 1
getName()           // 1
new Foo.getName()   // 2
new Foo().getName()     // 3
new new Foo().getName() // 3
  • 第一问:Foo.getName 自然是访问 Foo 函数上存储的静态属性,自然是 2
  • 第二问:直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫 getName 的函数,所以跟 1 2 3 都没什么关系。但是此处有两个坑,一是变量声明提升,二是函数表达式。关于函数变量提示,此处省略一万字。。。。题中代码最终执行时的是
function Foo() {
  getName = function() {
    alert(1)
  }
  return this
}
var getName //只提升变量声明
function getName() {
  alert(5)
} //提升函数声明,覆盖var的声明

Foo.getName = function() {
  alert(2)
}
Foo.prototype.getName = function() {
  alert(3)
}
getName = function() {
  alert(4)
} //最终的赋值再次覆盖function getName声明
  • 第三问: Foo().getName(); 先执行了 Foo 函数,然后调用 Foo 函数的返回值对象的 getName 属性函数。这里 Foo 函数的返回值是 this,this 指向 window 对象。所以第三问相当于执行 window.getName()。 然而这里 Foo 函数将此变量的值赋值为function(){alert(1)}
  • 第四问:直接调用 getName 函数,相当于 window.getName(),答案和前面一样。

后面三问都是考察 js 的运算符优先级问

面向对象 --- 继承

继承:继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。

比如我有个构造函数A,然后又有个构造函数B,但是B想要使用A里的一些属性和方法,一种办法就是让我们自身化身为CV侠,复制粘贴一波。还有一种就是利用继承,我让B直接继承了A里的功能,这样我就能用它了。

  • 1. 原型链继承
  • 2. 构造继承
  • 3. 组合继承
  • 4. 寄生组合继承
  • 5. 原型式继承
  • 6. 寄生继承
  • 7. 混入式继承
  • 8. class中的extends继承

原型链继承

instanceof 关键字

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上 

A instanceof B 
实例对象A instanceof 构造函数B
检测A的原型链(__proto__)上是否有B.prototype,有则返回true,否则返回false

class中的继承:extends、super

面向对象 --- 多态

多态的实际含义是:同一操作作用于不同的对象上,可以产生不同的解释和不同的执行结果。

class中的多态:extends、super

对于js多态的详细解释:https://juejin.cn/post/6844904126011146254

this new

this 永远指向最后调用它的那个对象

this,是指当前的本身,在非严格模式下this指向的是全局对象window,而在严格模式下会绑定到undefined。

this 的 5种绑定方式:

  • 默认绑定 ( 非严格模式下this指向全局对象,严格模式下 this 会绑定到 undefined )
  • 隐式绑定 ( 当函数引用有上下文对象时, 如 obj.foo() 的调用方式,foo内的 this指向obj )
  • 显示绑定 ( 通过 call() 或者 apply() 方法直接指定 this 的绑定对象, 如 foo.call(obj) )
  • new 绑定
  • 箭头函数绑定 ( this 的指向由外层作用域决定的,并且指向函数定义时的 this,而不是执行时的 this)

再次强调:

  • this 永远指向最后调用它的那个对象
  • 匿名函数的 this 永远指向 window
  • 使用 call() 或者 apply() 的函数会直接执行
  • bing() 是创建一个新的函数,需要手动调用才会执行
  • 如果 call、appy、bind 接收到的第一个参数是空或者 null,undefine 的话,则会忽略这个参数
  • forEach、map、filter 函数的第二个参数也是能显式绑定 this 的

默认绑定

没有绑定到任何对象的 变量、函数、属性 等,都是 绑定到 window 对象。

var a = 10;  // a 属于 window 
function foo(){
    console.log(this.a);  // this 指向 window
    console.log(this);    // window 对象
    console.log(window)   // window 对象
    console.log(this === window); // true
}
foo();  // 10
console.log(window.a);  // 10
console.log(window.a === this.a);  // true

使用 let 、const 声明的 变量,不会绑定到 window

let a = 10;
const b = 20;
function foo(){
    console.log(this.a);
    console.log(this.b);
}
foo();
console.log(window.a);

var a = 1;
function foo(){
    var a = 2
    console.log(this);
    console.log(this.a); // foo 属于 window ,所以 打印 1
}
foo();

修改代码:

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

foo 函数属于 window,inner 是 foo 的内嵌函数,所以 this 指向 window 

隐式绑定

示例代码:

function foo(){
    console.log(this.a);
}
var obj = {a:1, foo};  // var obj = {foo}  等价于 var obj = {foo: foo}
var a = 2;
obj.foo();  // 打印 1

隐式丢失:就是被隐式绑定的函数,在特定的情况下会丢失绑定对象。

特定情况是指:

  • 1. 使用另一个变量来给函数取别名
  • 2. 或者 将函数作为参数传递时会被隐式赋值,回调函数丢失 this 绑定

示例:

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

obj.foo();  // 1
foo2();     // 2  这里 foo2(); 是被 window 调用的,即 window.foo2(); 使的 this 指向 window

示例:

function foo(){
    console.log(this.a);
}
function doFoo(fn){
    console.log(this);
    fn();
}
var obj = {a:1, foo};
var a = 2;
doFoo(obj.foo);  // 打印 2

示例:

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

var obj2 = {a:3, doFoo};
obj2.doFoo(obj.foo);  // 打印 2

结论:如果把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的 this 指向无关。在非严格模式下,会把该函数的 this 绑定到 window 上。严格模式下绑定到 undefine

示例:

var obj1 = {
    a:1
};
var obj2 = {
    a:2,
    foo1: function(){
        console.log(this.a);  // 打印 2
    },
    foo2: function(){
        setTimeout(function(){
            console.log(this);  // 打印 window 对象
            console.log(this.a);  // 打印 3
         }, 0);
    }
};
var a =3;
obj2.foo1();
obj2.foo2();   // this 永远指向最后调用它的那个对象。。。

call, apply, bind

使用 call() 或者 apply()  的函数是会直接执行的。
bind() 是创建一个新的函数,需要手动调用才会执行

  • 1. 都是对函数的操作,使用方式:函数.call
  • 2. 都是用来改变函数的 this 对象的指向的。
  • 3. 第一个参数都是 this 要指向的对象。
  • 4. 都可以利用后续参数传参。
  • 5. call 接受函数传参方式为:fn.call(this, 1, 2, 3)
  • 6. apply 接受函数传参方式为:fn.apply(this,[1, 2, 3])
  • 7. bind 的返回值为一个新的函数,需要再次调用: fn.bind(this)(1, 2, 3)

专利局( JS 混淆,大量运用原型链 )( F12 打开控制台 ):http://cpquery.sipo.gov.cn/

示例( 函数内嵌套的函数中的 this 指向 window ):

var obj1 = {
    a:1
};
var obj2 = {
    a:2,
    foo1: function(){
        console.log(this.a);
    },
    foo2: function(){
        var that = this;
        console.log(that);  // 打印 obj2 对象
        function inner(){
            console.log(this);   // 打印 window 对象
            console.log(this.a); // 打印 window 对象 的 a
        }
        inner();
        inner.call(obj2);  // 改变 this 指向 obj2, 所以 打印 2
    }
};
var a =3;
obj2.foo1();  // 打印 2
obj2.foo2();  // 打印 3

示例:

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

foo();
foo.call(obj);
foo().call(obj);  // foo的返回值是一个函数,再进行call,改变this指向

//结果:
// 2  --->  foo(); 输出结果
// 1  --->  foo().call(obj); 改变this指向 obj, 输出 1
// 2  --->  
// 1  --->  foo 的返回值,再进行call,改变this指向

new 绑定

function Person(name){
	this.name = name;
	this.foo1 = function(){
		console.log(this.name);
	};
	this.foo2 = function(){
		return function(){
			console.log(this.name);
		};
	};
}
var person1 = new Person('person1');
person1.foo1();
person1.foo2()();

new 和 call 同时出现

var name = 'window'
function Person(name){
	this.name = name;
	this.foo = function(){
		console.log(this.name);
        return function(){
            console.log(this.name);
        };
	};
}
var person1 = new Person('person1');
var person2 = new Person('person2');

person1.foo.call(person2)();
person1.foo().call(person2);

箭头函数

箭头函数绑定中:this 的指向由外层作用域决定的,并且指向函数定义时的 this,而不是执行时的 this。

var obj = {
    name: 'obj',
    foo1: () => {
        console.log(this.name);
    },
    foo2: function(){
        console.log(this.name);
        return () => {console.log(this.name)};
    }
};
var name = 'window';
obj.foo1();
obj.foo2()();

结果:
window
obj
obj

示例:

var name = 'window';
function Person(name){
    this.name = name;
    this.foo1 = function(){
        console.log(this.name);
    };
    this.foo2 = ()=>{console.log(this.name);};    
}
var Person2 = {
    name: 'Person2',
    foo2: ()=>{ console.log(this.name); }
};
var person1 = new Person('person1');
person1.foo1();
person1.foo2();
Person2.foo2();

结果:
person1
person1
window

示例:( 箭头函数 与 call 结合 )

箭头函数里面的 this 是由外层作用域来决定的,并且指向函数定义时的 this,而不是执行时的 this
字面量创建的对象,作用域是 window,如果里面有箭头函数属性的话, this  指向的是 window
构造函数创建的对象,作用域是可以理解为是这个构造函数,且这个构造函数的 this 是指向新建的对象的,因此 this 指向这个对象
箭头函数的 this 是无法通过 bind、call、apply 来直接修改,但是可以用过改变作用域中 this 的指向来间接修改

var name = 'window';
var obj1 = {
	name: 'obj1',
	foo1: function(){
		console.log(this.name);
		return ()=>{ console.log(this.name); }
	},
	foo2: ()=>{
		console.log(this.name);
		return function(){
			console.log(this.name);
		}
	}
}
var obj2 = { name: 'obj2' }

obj1.foo1.call(obj2)()
obj1.foo1().call(obj2)
obj1.foo2.call(obj2)()
obj1.foo2().call(obj2)

结果:
obj2
obj2
obj1
obj1
window
window
window
obj2

new 的过程中到底发生了什么?

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

更多可以参看 《JavaScript高级程序设计 第4版》

js 逆向技巧

From:http://t.zoukankan.com/Renyi-Fan-p-12650448.html

一、总结

一句话总结:

1、搜索;2、debug;3、查看请求调用的堆栈;4、执行堆内存中的函数;5、修改堆栈中的参数值;6、写js代码;7、打印windows对象的值;8、勾子

  • 1. 搜索:全局搜索、代码内搜索
  • 2. debug:常规debug、XHR debug、行为debug
  • 3. 查看请求调用的堆栈
  • 4. 执行堆内存中的函数
  • 5. 修改堆栈中的参数值
  • 6. 写js代码
  • 7. 打印windows对象的值
  • 8. 勾子:cookie钩子、请求钩子、header钩子

二、js逆向技巧

博客对应课程的视频位置:

当我们抓取网页端数据时,经常被加密参数、加密数据所困扰,如何快速定位这些加解密函数,尤为重要。本片文章是我逆向js时一些技巧的总结,如有遗漏,欢迎补充。

所需环境:Chrome浏览器

1. 搜索

1.1 全局搜索 ( Ctrl + shift + f

适用于根据关键词快速定位关键文件及代码

当前页面 右键 ---> 检查,弹出检查工具

搜索支持 关键词、正则表达式

1.2 代码内搜索 ( Ctrl + f

适用于根据关键词快速定位关键代码

点击代码,然后按 ctrl+f 或 command+f 调出搜索框。搜索支持 关键词、css表达式、xpath

2. debug

2.1 常规 debug

适用于分析关键函数代码逻辑

a、埋下断点

b、调试

如图所示,标记了 1 到 6,下面分别介绍其含义

  • 1. 执行到下一个端点
  • 2. 执行下一步,不会进入所调用的函数内部
  • 3. 进入所调用的函数内部
  • 4. 跳出函数内部
  • 5. 一步步执行代码,遇到有函数调用,则进入函数
  • 6.Call Stack 为代码调用的堆栈信息,代码执行顺序为由下至上,这对于着关键函数前后调用关系很有帮助

2.2 XHR debug

匹配url中关键词,匹配到则跳转到参数生成处,适用于url中的加密参数全局搜索搜不到,可采用这种方式拦截

2.3 行为 debug

适用于点击按钮时,分析代码执行逻辑

如图所示,可快速定位点击探索按钮后,所执行的js。

3 查看请求调用的堆栈

可以在 Network 选项卡下,该请求的 Initiator 列里看到它的调用栈,调用顺序由上而下:

4. 执行堆内存中的函数

当 debug 到某一个函数时,我们想主动调用,比如传递下自定义的参数,这时可以在检查工具里的 console 里调用

此处要注意,只有debug打这个函数时,控制台里才可以调用。如果想保留这个函数,可使用this.xxx=xxx 的方式。之后调用时无需debug到xxx函数,直接使用this.xxx 即可。

5. 修改堆栈中的参数值

6. 写 js 代码

7. 打印 windows 对象的值

在 console 中输入如下代码,如只打印 _$ 开头的变量值

for (var p in window) {
    if (p.substr(0, 2) !== "_$") 
        continue;
    console.log(p + " >>> " + eval(p))
}

8. 勾子

以 chrome 插件的方式,在匹配到关键词处插入断点

8.1 cookie 钩子

用于定位 cookie 中关键参数生成位置

var code = function(){
    var org = document.cookie.__lookupSetter__('cookie');
    document.__defineSetter__("cookie",function(cookie){
        if(cookie.indexOf('TSdc75a61a')>-1){
            debugger;
        }
        org = cookie;
    });
    document.__defineGetter__("cookie",function(){return org;});
}
var script = document.createElement('script');
script.textContent = '(' + code + ')()';
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);

当 cookie 中匹配到了 TSdc75a61a, 则插入断点。

8.2 请求钩子

用于定位请求中关键参数生成位置

var code = function(){
var open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, url, async){
    if (url.indexOf("MmEwMD")>-1){
        debugger;
    }
    return open.apply(this, arguments);
};
}
var script = document.createElement('script');
script.textContent = '(' + code + ')()';
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);

当请求的 url 里包含 MmEwMD 时,则插入断点

8.3 header 钩子

用于定位 header 中关键参数生成位置

var code = function(){
var org = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function(key,value){
    if(key=='Authorization'){
        debugger;
    }
    return org.apply(this,arguments);
}
}
var script = document.createElement('script');
script.textContent = '(' + code + ')()';
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);

当 header 中包含 Authorization 时,则插入断点

8.4 manifest.json

插件的配置文件

{
   "name": "Injection",
    "version": "2.0",
    "description": "RequestHeader钩子",
    "manifest_version": 2,
    "content_scripts": [
        {
            "matches": [
                "<all_urls>"
            ],
            "js": [
                "inject.js"
            ],
            "all_frames": true,
            "permissions": [
                "tabs"
            ],
            "run_at": "document_start"
        }
    ]
}

使用方法

a、如图所示,创建一个文件夹,文件夹中创建一个钩子函数文件inject.js 及 插件的配置文件 mainfest.json 即可

b、打开chrome 的扩展程序, 加载已解压的扩展程序,选择步骤1创建的文件夹即可

c、切换回原网页,刷新页面,若钩子函数关键词匹配到了,则触发debug

猜你喜欢

转载自blog.csdn.net/freeking101/article/details/116665051