js 逆向:https://www.cnblogs.com/wuxianyu/category/1940304.html
js逆向2:https://www.cnblogs.com/wuxianyu/category/1941486.html
JavaScript 逆向 系列:https://blog.csdn.net/yy_rose/category_11919548.html
JavaScript混淆安全加固:https://juejin.cn/post/6844903861958737927
asm.js 和 Emscripten 入门教程:https://ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
网页是运行在浏览器端的,当我们浏览一个网页时,其 HTML代码、JavaScript代码都会被下载到浏览器中执行。借助浏览器的开发者工具,可以看到网页加载过程中所有网络请求的详细信息也能清楚地看到网站运行的HTML代码和JavaScript代码。这些代码里就包含了网站加载的全部逻辑比如加载哪些资源,请求接口是如何构造的,页面是如何渲染的,等等。正因为代码是完全透明的所以如果我们能研究明白其中的执行逻辑,就可以模拟各个网络请求,进行数据爬取了。
但是,事情没有想象得那么简单。随着前端技术的发展,前端代码的打包技术、混淆技术、加密技术也层出不穷,借助于这些技术,各个公司可以在前端对 JavaScript 代码采取一定的保护,比如变量名混淆、执行逻辑混淆、反调试、核心逻辑加密等,这些保护手段使得我们没法很轻易地找出JavaScript代码中包含的执行逻辑。
压缩、混淆、加密
- 代码压缩:去除JavaScript代码中不必要的空格、换行等内容,使源码都压缩为几行内容,降低代码的可读性,当然同时也能提高网站的加载速度。目前主流的前端开发技术大多都会利用webpack、Rollup等工具进行打包。webpack、Rollup会对源代码进行编译和压缩输出几个打包好的JavaScript文件,其中我们可以看到输出的JavaScript文件名带有一些不规则的字符串,同时文件内容可能只有几行,变量名都用一些简单字母表示。这其中就包含JavaScript压缩技术,比如一些公共的库输出成 bundle文件,一些调用逻辑压缩和转义成冗长的几行代码这些都属于JavaScript压缩。另外其中也包含了一些很基础的JavaScript混淆技术,比如把变量名、方法名替换成一些简单字符,降低代码的可读性。
- 代码混淆:使用变量替换、字符串阵列化、控制流平坦化、多态变异、尸函数、调试保护等手段,使代码变得难以阅读和分析。达到最终保护的目的。但这不影响代码的原有功能,是理想、实用的JavaScript保护方案。
- 代码加密:可以通过某种手段将 JavaScript代码进行加密,转成人无法阅读或者解析的代码如借用WebAssembly技术,可以直接将JavaScript代码用C/C++实现JavaScript调用其编译后形成的文件来执行相应的功能。
JavaScript 混淆技术主要有以下几种
- 变量名混淆:将带有含义的变量名、方法名、常量名随机变为无意义的类乱码字符串降低代码的可读性,如转成单个字符或十六进制字符串。
- 字符串混淆:将字符串阵列化集中放置并可进行MD5或Base64加密存储使代码中不出现明文字符串,这样可以避免使用全局搜索字符串的方式定位到入口。
- 对象键名替换:针对JavaScript对象的属性进行加密转化,隐藏代码之间的调用关系口 控制流平坦化:打乱函数原有代码的执行流程及函数调用关系,使代码逻辑变得混乱无序。口 无用代码注入:随机在代码中插人不会被执行到的无用代码,进一步使代码看起来更加混乱。口调试保护:基于调试器特性,对当前运行环境进行检验,加人一些debugger 语句,使其在调试模式下难以顺利执行 JavaSeript代码。
- 多态变异:使JavaScript代码每次被调用时,将代码自身立刻自动发生变异,变为与之前完全不同的代码,即功能完全不变,只是代码形式变异,以此杜绝代码被动态分析和调试。域名锁定:使JavaScript代码只能在指定域名下执行。
- 代码自我保护:如果对JavaScript代码进行格式化,则无法执行,导致浏览器假死口特殊编码:将JavaScript完全编码为人不可读的代码,如表情符号、特殊表示内容,等等总之,以上方案都是JavaScript 混淆的实现方式,可以在不同程度上保护JavaScript代码。
在前端开发中现在JavaScript混淆的主流实现是 javascript-obfuscator 和 terser 这两个库。
WebAssembly
- 随着技术的发展 WebAssembly 逐渐流行起来。不同于JavaScript混淆技术,WebAssembly的基本思路是将一些核心逻辑使用其他语言(如C/C++语言)来编写,并编译成类似字节码的文件,并通过JavaScript 调用执行,从而起到二进制级别的防护作用。WebAssembly 是一种可以使用非 JavaScript 编语言编写代码并且能在浏览器上运行的技术方案。比如我们能将C/C+文件利用Emscripten 编译工具转成wasm格式的文件,JavaScript可以直接调用该文件执行其中的方法。WebAssembly 是经过编译器编译之后的字节码可以从C/C+编译而来得到的字节码具有和JavaScript相同的功能,运行速度更快,体积更小,而且在语法上完全脱离JavaScript,同时具有沙盒化的执行环境。
总结:WebAssembly 是经过编译器编译之后的字节码可以从C/C+编译而来得到的字节码具有和JavaScript相同的功能,运行速度更快,体积更小,而且在语法上完全脱离JavaScript,同时具有沙盒化的执行环境。
1、JavaScript 基础
菜鸟教程 JavaScript 教程:https://www.runoob.com/js/js-tutorial.html
- 1. 基础数据类型:number、string、boolean、null、undefined、object
- 2. 顺序、条件、循环、比较
- 3. 函数
- 4. 运算符
js 数组遍历方法总结:https://www.cnblogs.com/woshidouzia/p/9304603.html
JavaScript 作用域:
https://www.h5w3.com/57138.html
https://www.h5w3.com/62830.html
https://www.h5w3.com/70576.html
https://www.h5w3.com/34296.html
https://www.h5w3.com/32056.html
https://juejin.cn/post/6844904033308655623
强烈推荐书籍《JavaScript高级程序设计 第4版》:https://book.douban.com/subject/35175321/
示例:
Javascript 函数
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023021053637728
JavaScript 中的函数是`头等公民`,不仅可以像变量一样使用它,同时它还具有十分强大的抽象能力;
定义函数的 2 种方式
在JavaScript 中,定义函数的方式如下:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
上述abs()
函数的定义如下:
function
指出这是一个函数定义;abs
是函数的名称;(x)
括号内列出函数的参数,多个参数以,
分隔;{ ... }
之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。
请注意,函数体内部的语句在执行时,一旦执行到return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为undefined
。
由于JavaScript的函数也是一个对象,上述定义的abs()
函数实际上是一个函数对象,而函数名abs
可以视为指向该函数的变量。
因此,第二种定义函数的方式如下:
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};
在这种方式下,function (x) { ... }
是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量abs
,所以,通过变量abs
就可以调用该函数。
注意:上述两种定义完全等价,第二种方式按照完整语法需要在函数体末尾加一个
;
,表示赋值语句结束。( 加不加都一样,如果按照语法完整性要求,需要加上 )
调用函数
调用函数时,按顺序传入参数即可:
abs(10); // 返回10
abs(-9); // 返回9
由于JavaScript 允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
传入的参数比定义的少也没有问题:
abs(); // 返回NaN
此时 abs(x)
函数的参数x
将收到undefined
,计算结果为NaN
。要避免收到 undefined
,可以对参数进行检查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}
if (x >= 0) {
return x;
} else {
return -x;
}
}
arguments
JavaScript 还有一个免费赠送的关键字arguments
,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments
类似 Array
但它不是一个Array
:
'use strict'
function foo(x) {
console.log('x = ' + x); // 10
for (var i=0; i<arguments.length; i++) {
console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30
}
}
foo(10, 20, 30);
利用arguments
,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值,实际上arguments
最常用于判断传入参数的个数。
rest 参数
由于 JavaScrip t函数允许接收任意个参数,于是我们就不得不用arguments
来获取所有参数:
'use strict'
function foo(a, b) {
var i, rest = [];
if (arguments.length > 2) {
for (i = 2; i<arguments.length; i++) {
rest.push(arguments[i]);
}
}
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
为了获取除了已定义参数a
、b
之外的参数,我们不得不用arguments
,并且循环要从索引2
开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的rest
参数,有没有更好的方法?
ES6 标准引入了rest参数,上面的函数可以改写为:
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]
foo(1);
// 结果:
// a = 1
// b = undefined
// Array []
rest参数只能写在最后,前面用...
标识,从运行结果可知,传入的参数先绑定a
、b
,多余的参数以数组形式交给变量rest
,所以,不再需要arguments
我们就获取了全部参数。
如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined
)。
因为rest参数是ES6新标准,所以你需要测试一下浏览器是否支持。请用rest参数编写一个sum()
函数,接收任意个参数并返回它们的和:
'use strict'
function sum(...rest) {
console.log(rest)
}
sum(1,2,3,4,5)
测试:
// 测试:
var i, args = [];
for (i=1; i<=100; i++) {
args.push(i);
}
if (sum() !== 0) {
console.log('测试失败: sum() = ' + sum());
} else if (sum(1) !== 1) {
console.log('测试失败: sum(1) = ' + sum(1));
} else if (sum(2, 3) !== 5) {
console.log('测试失败: sum(2, 3) = ' + sum(2, 3));
} else if (sum.apply(null, args) !== 5050) {
console.log('测试失败: sum(1, 2, 3, ..., 100) = ' + sum.apply(null, args));
} else {
console.log('测试通过!');
}
小心你的 return 语句
JavaScript 引擎有一个在行末自动添加分号的机制,
这可能让你栽到return语句的一个大坑。。。
示例:
function foo() {
return { name: 'foo' };
}
foo(); // { name: 'foo' }
如果把 return 语句拆成两行:
function foo() {
return
{ name: 'foo' };
}
foo(); // undefined
要小心了,由于 JavaScript 引擎在行末自动添加分号的机制,上面的代码实际上变成了:
function foo() {
return; // 自动添加了分号,相当于return undefined;
{ name: 'foo' }; // 这行语句已经没法执行到了
}
所以正确的多行写法是:
function foo() {
return { // 这里不会自动加分号,因为{表示语句尚未结束
name: 'foo'
};
}
变量作用域与解构赋值
在 JavaScript 中,用var声明
的变量实际上是有作用域的。
如果一个变量在函数体内部声明
,则该变量的作用域为整个函数体,在函数体外不可引用该变量:
'use strict';
function foo() {
var x = 1;
x = x + 1;
}
x = x + 2; // ReferenceError! 无法在函数体外引用变量x
如果两个不同的函数各自声明
了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响:
'use strict';
function foo() {
var x = 1;
x = x + 1;
}
function bar() {
var x = 'A';
x = x + 'B';
}
由于 JavaScript 的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行:
'use strict';
function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以访问foo的变量x!
}
var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}
如果内部函数和外部函数的变量名重名怎么办?来测试一下:
'use strict'
function foo() {
var x = 1;
function bar() {
var x = 'A';
console.log('x in bar() = ' + x); // 'A'
}
console.log('x in foo() = ' + x); // 1
bar();
}
foo();
/*
x in foo() = 1
x in bar() = A
*/
这说明 JavaScript 的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。
变量提升
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:
'use strict';
function foo() {
var x = 'Hello, ' + y;
console.log(x);
var y = 'Bob';
}
foo();
虽然是strict模式,但语句var x = 'Hello, ' + y;
并不报错,原因是变量y
在稍后申明了。但是console.log
显示Hello, undefined
,说明变量y
的值为undefined
。这正是因为JavaScript引擎自动提升了变量y
的声明,但不会提升变量y
的赋值。
对于上述foo()
函数,JavaScript引擎看到的代码相当于:
function foo() {
var y; // 提升变量y的申明,此时y为undefined
var x = 'Hello, ' + y;
console.log(x);
y = 'Bob';
}
由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var
申明函数内部用到的所有变量:
function foo() {
var
x = 1, // x初始化为1
y = x + 1, // y初始化为2
z, i; // z和i为undefined
// 其他语句:
for (i=0; i<100; i++) {
...
}
}
全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript 默认有一个全局对象 window
,全局作用域的变量实际上被绑定到 window
的一个属性。 在 nodejs 中,是绑定到 global 这个变量中
'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
因此,直接访问全局变量course
和访问window.course
是完全一样的。
你可能猜到了,由于函数定义有两种方式,以变量方式var foo = function () {}
定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到window
对象:
'use strict';
function foo() {
alert('foo');
}
foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用
进一步大胆地猜测,我们每次直接调用的alert()
函数其实也是window
的一个变量:
'use strict';
window.alert('调用window.alert()');
// 把alert保存到另一个变量:
var old_alert = window.alert;
// 给alert赋一个新函数:
window.alert = function () {} // alert('无法用alert()显示了!');
恢复 alert
// 恢复alert:
window.alert = old_alert;
alert('又可以用alert()了!');
这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError
错误。
名字空间
全局变量会绑定到window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP:
var MYAPP = {};
// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函数:
MYAPP.foo = function () {
return 'foo';
};
把自己的代码全部放入唯一的名字空间MYAPP
中,会大大减少全局变量冲突的可能。
许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。
局部作用域
由于JavaScript的变量作用域实际上是函数内部,我们在for
循环等语句块中是无法定义具有局部作用域的变量的:
'use strict';
function foo() {
for (var i=0; i<100; i++) {
//
}
i += 100; // 仍然可以引用变量i
}
为了解决块级作用域,ES6引入了新的关键字let
,用let
替代var
可以申明一个块级作用域的变量:
'use strict';
function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
// SyntaxError:
i += 1;
}
常量
由于var
和let
申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:
var PI = 3.14;
ES6标准引入了新的关键字const
来定义常量,const
与let
都具有块级作用域:
'use strict';
const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14
解构赋值
从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。
什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:
var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];
现在,在ES6中,可以使用解构赋值,直接对多个变量同时赋值:
'use strict'
// 如果浏览器支持解构赋值就不会报错
// x, y, z分别被赋值为数组对应元素:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
console.log('x = ' + x + ', y = ' + y + ', z = ' + z);
注意,对数组元素进行解构赋值时,多个变量要用[...]
括起来。
如果数组本身还有嵌套,也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致:
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'
解构赋值还可以忽略某些元素:
let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'
如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:
'use strict'
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
var {name, age, passport} = person;
// name, age, passport分别被赋值为对应属性:
console.log('name = ' + name + ', age = ' + age + ', passport = ' + passport);
对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school',
address: {
city: 'Beijing',
street: 'No.1 Road',
zipcode: '100001'
}
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性:
address; // Uncaught ReferenceError: address is not defined
使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined
,这和引用一个不存在的属性获得undefined
是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
// 把passport属性赋值给变量id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量,而是为了让变量id获得passport属性:
passport; // Uncaught ReferenceError: passport is not defined
解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined
的问题:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678'
};
// 如果person对象没有single属性,默认赋值为true:
var {name, single=true} = person;
name; // '小明'
single; // true
有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:
// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =
这是因为JavaScript引擎把{
开头的语句当作了块处理,于是=
不再合法。解决方法是用小括号括起来:
({x, y} = { name: '小明', x: 100, y: 200});
使用场景
解构赋值在很多时候可以大大简化代码。例如,交换两个变量x
和y
的值,可以这么写,不再需要临时变量:
var x=1, y=2;
[x, y] = [y, x]
快速获取当前页面的域名和路径:
var {hostname:domain, pathname:path} = location;
如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个Date
对象:
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
它的方便之处在于传入的对象只需要year
、month
和day
这三个属性:
buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
也可以传入hour
、minute
和second
属性:
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)
使用解构赋值可以减少代码量,但是,需要在支持ES6解构赋值特性的现代浏览器中才能正常运行。目前支持解构赋值的浏览器包括Chrome,Firefox,Edge等。
补充几个两数交换的方法
// 方法 1
b=[a,a=b][0];
// 方法 2
a=a+b-(b=a);
// 方法 3
a=a^b;
b=b^a;
a=a^b;
方法( 对象中的函数叫做方法 )
在一个对象中绑定函数,称为这个对象的方法。
在JavaScript中,对象的定义是这样的:
var xiaoming = {
name: '小明',
birth: 1990
};
但是,如果我们给xiaoming
绑定一个函数,就可以做更多的事情。比如,写个age()
方法,返回xiaoming
的年龄:
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年调用是25,明年调用就变成26了
绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个this
关键字,这个东东是什么?
在一个方法内部,this
是一个特殊变量,它始终指向当前对象,也就是xiaoming
这个变量。所以,this.birth
可以拿到xiaoming
的birth
属性。
让我们拆开写:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常结果
getAge(); // NaN
单独调用函数getAge()
怎么返回了NaN
?请注意,我们已经进入到了JavaScript的一个大坑里。
JavaScript的函数内部如果调用了this
,那么这个this
到底指向谁?
答案是,视情况而定!
如果以对象的方法形式调用,比如xiaoming.age()
,该函数的this
指向被调用的对象,也就是xiaoming
,这是符合我们预期的。
如果单独调用函数,比如getAge()
,此时,该函数的this
指向全局对象,也就是window
。
坑爹啊!
更坑爹的是,如果这么写:
var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN
也是不行的!要保证this
指向正确,必须用obj.xxx()
的形式调用!
由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的this
指向undefined
,因此,在strict模式下,你会得到一个错误:
'use strict';
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
var fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined
这个决定只是让错误及时暴露出来,并没有解决this
应该指向的正确位置。
有些时候,喜欢重构的你把方法重构了一下:
'use strict';
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
结果又报错了!原因是this
指针只在age
方法的函数内指向xiaoming
,在函数内部定义的函数,this
又指向undefined
了!(在非strict模式下,它重新指向全局对象window
!)
修复的办法也不是没有,我们用一个that
变量首先捕获this
:
'use strict';
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法内部一开始就捕获this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
用var that = this;
,你就可以放心地在方法内部定义其他函数,而不是把所有语句都堆到一个方法中。
apply
虽然在一个独立的函数调用中,根据是否是strict模式,this
指向undefined
或window
,不过,我们还是可以控制this
的指向的!
要指定函数的this
指向哪个对象,可以用函数本身的apply
方法,它接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
用apply
修复getAge()
调用:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
另一个与apply()
类似的方法是call()
,唯一区别是:
-
apply()
把参数打包成Array
再传入; -
call()
把参数按顺序传入。
比如调用Math.max(3, 5, 4)
,分别用apply()
和call()
实现如下:
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
对普通函数调用,我们通常把this
绑定为null
。
装饰器
利用apply()
,我们还可以动态改变函数的行为。
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
现在假定我们想统计一下代码一共调用了多少次parseInt()
,可以把所有的调用都找出来,然后手动加上count += 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()
:
'use strict'
var count = 0;
var oldParseInt = parseInt; // 保存原函数
var window = global
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};
// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3
高阶函数
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023021271742944
高阶函数英文叫Higher-order function。那么什么是高阶函数?
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
function add(x, y, f) {
return f(x) + f(y);
}
当我们调用add(-5, 6, Math.abs)
时,参数x
,y
和f
分别接收-5
,6
和函数Math.abs
,根据函数定义,我们可以推导计算过程为:
x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;
map / reduce
map()
方法定义在JavaScript的Array
中,我们调用Array
的map()
方法,传入我们自己的函数,就得到了一个新的Array
作为结果:
'use strict'
function pow(x) {
return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);
filter
用于把Array
的某些元素过滤掉,然后返回剩下的元素。
和map()
类似,Array
的filter()
也接收一个函数。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是true
还是false
决定保留还是丢弃该元素。
例如,在一个Array
中,删掉偶数,只保留奇数,可以这么写:
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]
把一个Array
中的空字符串删掉,可以这么写:
var arr = ['A', '', 'B', null, undefined, 'C', ' '];
var r = arr.filter(function (s) {
return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
});
r; // ['A', 'B', 'C']
可见用filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。
回调函数
filter()
接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示Array
的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身:
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是变量arr
return true;
});
利用filter
,可以巧妙地去除Array
的重复元素:
'use strict'
var r, arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function (element, index, self) {
return self.indexOf(element) === index;
});
console.log(r.toString());
去除重复元素依靠的是indexOf
总是返回第一个元素的位置,后续的重复元素位置与indexOf
返回的位置不相等,因此被filter
滤掉了。
Array
对象的其他高阶函数
对于数组,除了map()
、reduce
、filter()
、sort()
这些方法可以传入一个函数外,Array
对象还提供了很多非常实用的高阶函数。
every
every()
方法可以判断数组的所有元素是否满足测试条件。
例如,给定一个包含若干字符串的数组,判断所有字符串是否满足指定的测试条件:
'use strict'
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.every(function (s) {
return s.length > 0;
})); // true, 因为每个元素都满足s.length>0
console.log(arr.every(function (s) {
return s.toLowerCase() === s;
})); // false, 因为不是每个元素都全部是小写
find
find()
方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined
:
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.find(function (s) {
return s.toLowerCase() === s;
})); // 'pear', 因为pear全部是小写
console.log(arr.find(function (s) {
return s.toUpperCase() === s;
})); // undefined, 因为没有全部是大写的元素
findIndex
findIndex()
和find()
类似,也是查找符合条件的第一个元素,不同之处在于findIndex()
会返回这个元素的索引,如果没有找到,返回-1
:
var arr = ['Apple', 'pear', 'orange'];
console.log(arr.findIndex(function (s) {
return s.toLowerCase() === s;
})); // 1, 因为'pear'的索引是1
console.log(arr.findIndex(function (s) {
return s.toUpperCase() === s;
})); // -1
forEach
forEach()
和map()
类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。forEach()
常用于遍历数组,因此,传入的函数不需要返回值:
var arr = ['Apple', 'pear', 'orange'];
arr.forEach(console.log); // 依次打印每个元素
闭包
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023021250770016
函数作为返回值:高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
箭头函数
ES6 标准新增了一种新的函数:Arrow Function(箭头函数)。
为什么叫Arrow Function?因为它的定义用的就是一个箭头:
x => x * x
上面的箭头函数相当于:
function (x) {
return x * x;
}
在继续学习箭头函数之前,请测试你的浏览器是否支持 ES 6的 Arrow Function:
var fn = x => x * x;
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,
- 一种像上面的,只包含一个表达式,连
{ ... }
和return
都省略掉了。 - 还有一种可以包含多条语句,这时候就不能省略
{ ... }
和return
:
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}
如果参数不是一个,就需要用括号()
括起来:
// 两个参数:
(x, y) => x * x + y * y
// 无参数:
() => 3.14
// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
// SyntaxError:
x => { foo: x }
因为和函数体的{ ... }
有语法冲突,所以要改为:
// ok:
x => ({ foo: x })
this
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this
是词法作用域,由上下文确定。
回顾前面的例子,由于JavaScript函数对this
绑定的错误处理,下面的例子无法得到预期结果:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
};
现在,箭头函数完全修复了this
的指向,this
总是指向词法作用域,也就是外层调用者obj
:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
}
};
obj.getAge(); // 25
如果使用箭头函数,以前的那种hack写法:
var that = this;
就不再需要了。
由于this
在箭头函数中已经按照词法作用域绑定了,所以,用call()
或者apply()
调用箭头函数时,无法对this
进行绑定,即传入的第一个参数被忽略:
var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth; // this.birth仍是1990
return fn.call({birth:2000}, year);
}
};
obj.getAge(2015); // 25
generator
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023024381818112
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。
ES6定义generator标准的哥们借鉴了Python的generator的概念和语法,如果你对Python的generator很熟悉,那么ES6的generator就是小菜一碟了。如果你对Python还不熟,赶快恶补Python教程!。
定义 类(class)的几种方法
在面向对象编程中,类(class)是对象(object)的模板,定义了同一组对象(又称"实例")共有的属性和方法。类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。
早期的javascript
需求都很简单, 不支持面向对象,基本都是写成函数的,然后是面向过程的写法,后来慢慢的引入面向对象开发思想,再后来就慢慢写成 类
。
在 class 概念引入之前,js通过原型对象来实现类和类的继承,具体可以参考前文( 面向对象的 JavaScript:封装、继承与多态:https://zhuanlan.zhihu.com/p/112779427 )
在 ECMAScript 6 出现 class 的概念之后,才算是告别了直接通过原型对象来模拟类和类继承,但class 也只是基于 JavaScript 原型继承的语法糖,并没有引入新的对象继承模式,所以理解原型以及原型继承是非常重要的。通过 class 来创建对象,可以让代码更为简洁,复用性更高。
在js
中,写成类的本质基本都是 构造函数+原型
。下面,就讨论一下js类的几种写法:
构造函数 法
这是经典方法,也是教科书必教的方法。它用构造函数模拟 "类",在其内部用 this 关键字指代实例对象。
function Cat() {
this.name = "大毛";
}
生成实例的时候,使用 new 关键字。
var cat1 = new Cat();
alert(cat1.name); // 大毛
类的属性和方法,还可以定义在构造函数的 prototype 对象之上。
Cat.prototype.makeSound = function(){
alert("喵喵喵");
}
关于这种方法的详细介绍,查看系列文章《Javascript 面向对象编程》,这里就不多说了。它的主要缺点是,比较复杂,用到了 this 和 prototype,编写和阅读都很费力。
From:https://www.cnblogs.com/lidabo/archive/2011/12/17/2291238.html
一:定义类并创建类的实例对象
在Javascript中,我们用function来定义类,如下:
function Shape(){
var x = 1 ;
var y = 2 ;
}
你或许会说,疑?这个不是定义函数吗?没错,这个是定义函数,我们定义了一个 Shape 函数,并对x和y进行了初始化。不过,如果你换个角度来看,这个就是定义一个Shape类,里面有两个属性x和y,初始值分别是1和2,只不过,我们定义类的关键字是 function 而不是 class。然后,我们可以创建Shape类的对象aShape,如下:
var aShape = new Shape();
二:定义 公有属性、私有属性
我们已经创建了aShape对象,但是,当我们试着访问它的属性时,会出错,如下:
aShape.x = 1 ;
这说明,用 var 定义的属性是私有的。我们需要使用 this 关键字来定义公有的属性
function Shape(){
this .x = 1 ;
this .y = 2 ;
}
这样,我们就可以访问Shape的属性了,如:aShape.x = 2 ;
总结得到:用 var 可以定义类的private属性,而用 this 能定义类的 public 属性。
三:定义 公有方法、私有方法
在Javascript中,函数是 Function 类的实例,Function 间接继承自 Object,所以,函数也是一个对象,因此,我们可以用赋值的方法创建函数,当然,我们也可以将一个函数赋给类的一个属性变量,那么,这个属性变量就可以称为方法,因为它是一个可以执行的函数。代码如下:
function Shape()
{
var x = 0 ;
var y = 1 ;
this.draw = function (){
// print;
};
}
我们在上面的代码中定义了一个 draw,并把一个 function 赋给它,下面,我们就可以通过 aShape 调用这个函数,OOP 中称为 公有方法,如:aShape.draw();
如果用 var 定义,那么这个 draw 就变成私有的了,OOP 中称为私有方法,如
function Shape()
{
var x = 0 ;
var y = 1 ;
var draw = function (){
// print;
};
}
这样就不能使用aShape.draw调用这个函数了。
三:构造函数
Javascript 并不支持 OOP,当然也就没有构造函数了,不过,我们可以自己模拟一个构造函数,让对象被创建时自动调用,代码如下:
function Shape()
{
var init = function (){
// 构造函数代码
};
init();
}
在Shape的最后,我们人为的调用了init函数,那么,在创建了一个Shape对象是,init总会被自动调用,可以模拟我们的构造函数了。
四:带参数的构造函数
如何让构造函数带参数呢?其实很简单,将要传入的参数写入函数的参数列表中即可,如
function Shape(ax,ay)
{
var x = 0 ;
var y = 0 ;
var init = function (){
// 构造函数
x = ax;
y = ay;
};
init();
}
这样,我们就可以这样创建对象:var aShape = new Shape( 0 , 1 );
五:静态属性、静态方法
在 Javascript 中如何定义静态的属性和方法呢?如下所示
function Shape(ax,ay){
var x = 0 ;
var y = 0 ;
var init = function (){
// 构造函数
x = ax;
y = ay;
};
init();
}
Shape.count = 0 ; // 定义一个静态属性count,这个属性是属于类的,不是属于对象的。
Shape.staticMethod = function (){}; // 定义一个静态的方法
有了静态属性和方法,我们就可以用类名来访问它了,如下
alert(aShape.count);
aShape.staticMethod();
注意:静态属性和方法都是公有的,目前为止,我不知道如何让静态属性和方法变成私有的~
六:在方法中访问本类的公有属性和私有属性
在类的方法中访问自己的属性,Javascript对于公有属性和私有属性的访问方法有所不同,请大家看下面的代码
function Shape(ax,ay){
var x = 0 ;
var y = 0 ;
this .gx = 0 ;
this .gy = 0 ;
var init = function (){
x = ax; // 访问私有属性,直接写变量名即可
y = ay;
this .gx = ax; // 访问公有属性,需要在变量名前加上this.
this .gy = ay;
};
init();
}
七:this 的注意事项
在 JavaScript 中,类中的 this 并不是一直指向我们的这个对象本身的,主要原因还是因为Javascript 并不是 OOP 语言,而且,函数 和 类 均用 function 定义,当然会引起一些小问题。
this 指针指错的场合一般在事件处理上面,我们想让某个对象的成员函数来响应某个事件,当事件被触发以后,系统会调用我们这个成员函数,但是,传入的 this 指针已经不是我们本身的对象了,当然,这时再在成员函数中调用this当然会出错了。
解决方法是我们在定义类的一开始就将this保存到一个私有的属性中,以后,我们可以用这个属性代替this。我用这个方法使用this指针相当安全,而且很是省心~
我们修改一下代码,解决this问题。对照第六部分的代码看,你一定就明白了
function Shape(ax,ay)
{
var _this = this ; // 把this保存下来,以后用_this代替this,这样就不会被this弄晕了
var x = 0 ;
var y = 0 ;
_this.gx = 0 ;
_this.gy = 0 ;
var init = function ()
{
x = ax; // 访问私有属性,直接写变量名即可
y = ay;
_this.gx = ax; // 访问公有属性,需要在变量名前加上this.
_this.gy = ay;
};
init();
}
示例:
/***定义类***/
var Class = function(){
var _self = this;//把本身引用负值到一变量上
var _Field = "Test Field"; //私有字段
var privateMethod = function(){ //私有方法
alert(_self.Property); //调用属性
}
this.Property = "Test Property"; //公有属性
this.Method = function(){ //公有方法
alert(_Field); //调用私用字段
privateMethod(); //调用私用方法
}
/***构造函数***/
var init = function(){
privateMethod();
}
init();
}
// 使用这个类
var c = new Class();
c.Method(); // 使用方法
关于 Javascript 中的 OOP 实现就聊到这里,以上是最实用的内容,一般用 Javascript 定义类,创建对象用以上的代码已经足够了。当然,你还可以用 mootools 或 prototype 来定义类,创建对象。我用过mootools框架,感觉很不错,它对 Javascript 的类模拟就更完善了,还支持类的继承,有兴趣的读者可以去尝试一下。当然,如果使用了框架,那么在你的网页中就需要包含相关的js头文件,因此我还是希望读者能够在没有框架的情况下创建类,这样,代码效率较高,而且你也可以看到,要创建一个简单的类并不麻烦~
示例 1:
/**
* 封装类方法
* @methodOf Clazz.prototype
*/
var Clazz = function() {};
/**
* [给基类的原型赋值一个方法 当作类的构造器]
* @return {[Object]} [description]
*/
Clazz.prototype.construct = function() {};
/**
* 创建类
* @example
* var MyClass = Clazz.extend({
* //构造器,new时执行
* construct: function(myParam){
* // 编写你的代码逻辑
* }
* });
*
* 继承类
* var MySubClass = MyClass.extend({
* construct: function(myParam){
* // 使用这个来调用父类的构造函数
* arguments.callee.$.construct.apply(this, arguments);
* // 编写你的代码逻辑
* }
* });
*/
Clazz.extend = function(def) {
var classDef = function() {
if (arguments[0] !== Clazz) { this.construct.apply(this, arguments); }
};
var proto = new this(Clazz);
var superClass = this.prototype;
for (var n in def) {
var item = def[n];
if (item instanceof Function) item.$ = superClass;
proto[n] = item;
}
classDef.prototype = proto;
//给这个新类相同的静态扩展方法
classDef.extend = this.extend;
return classDef;
};
//========使用实例=========
var MyClass = Clazz.extend({
construct: function(options){
this.name = 'MyClass ';
this.myClassName = 'myClassName ';
},
getName: function(){
return this.name;
},
setName: function(name){
if(name) this.name = name;
}
});
//继承MyClass 类
var SubClass1 = MyClass .extend({
construct: function(){
//未调用父类的构造函数
this.name = 'SubClass ';
}
});
//继承MyClass 类
var SubClass2 = MyClass .extend({
construct: function(){
//调用父类构造函数
arguments.callee.$.construct.apply(this, arguments);
this.name = 'SubClass ';
}
});
var myClass = new MyClass();
var subClass1 = new SubClass1();
var subClass2 = new SubClass2();
console.log(myClass.getName()); //MyClass
console.log(myClass.myClassName); //myClassName
console.log(subClass1.getName()); //SubClass1
console.log(subClass1.myClassName); //undefined
console.log(subClass2.getName()); //SubClass2
console.log(subClass2.myClassName); //myClassName
示例 2:
/**
* Person类:定义一个人,有name属性和getName方法
*/
<script>
function Person(name){
this.name = name;
this.getName = function(){
return this.name;
}
}
//我们在这里实例化几个对象
var p1 = new Person("trigkit4");
var p2 = new Person("mike");
console.log(p1 instanceof Person);//true
console.log(p2 instanceof Person);//true
</script>
由上面控制台输出结果可知,p1和p2的确是类Person
的实例对象。instanceof
操作符左边是待检测类的对象,右边是定义类的构造函数。这里,instanceof
用来检测对象p1
是否属于Person
类。
这种方式的优点是:我们可以根据参数来构造不同的对象实例 ,缺点是每次构造实例对象时都会生成getName
方法,造成了内存的浪费 。
我们可以用一个外部函数来代替类方法,达到了每个对象共享同一个方法。改写后的类如下:
//外部函数
<script>
function getName() {
return this.name;
}
function Person(name){
this.name = name;
this.getName = getName;//
}
</script>
原型方式
<script>
function Person(){};
Person.prototype.name = "trigkit4";//类的属性都放在prototype上
Person.prototype.getName = function(){
return " I'm " + this.name;
}
var p1 = new Person();
var p2 = new Person();
console.log(p1.name);//trigkit4
console.log(p2.getName());//I'm trigkit4
</script>
原型方式:
- 缺点 就是不能通过参数来构造对象实例 (一般每个对象的属性是不相同的) ,
- 优点 是所有对象实例都共享getName方法(相对于构造函数方式),没有造成内存浪费 。
构造函数 + 原型方式
取前面两种的优点:
- a、用构造函数来定义类属性(字段)。
- b、用原型方式来定义类的方法。
<script>
function Person(name){
this.name = name;
}
//原型的特性可以让对象实例共享getName方法
Person.prototype.getName = function(){
return " I'm " + this.name;
}
</script>
这样,我们就既可以构造不同属性的对象,也可以让对象实例共享方法,不会造成内存的浪费。为了让js
代码风格更紧凑,我们让prototype
方法代码移到 function Person
的大括号内。
<script>
function Person(name){
this.name = name;
Person.prototype.getName = function(){
return this.name;
}
}
var p1 = new Person('trigkit4');
console.log(p1.getName());//trigkit4
</script>
示例:
/* 例1 */
// 定义一个构造函数
function Range(from, to){
this.from = from;
this.to = to;
}
// 所有Range类的实例化对象都会继承构造函数Range的prototype属性
Range.prototype = {
toString: function(){
return this.from + '....' + this.to;
},
includes: function(x){
return x >= this.from && x <= this.to;
}
};
// 实例化一个对象
var r = new Range(1, 3);
// 因为 r 继承了Range.prototype, 所以可以直接调用里面的方法
r.toString()
由 例1 和 例2 可以总结出javascript中定义类的步骤:
- 第一步:先定义一个构造函数,并设置初始化新对象的实例属性
- 第二步:给构造函数的prototype对象定义实例方法
- 第三步:给构造函数定义类字段和类属性
继承
新语法定义类,以及继承类
Object.create() 法
为了解决 "构造函数法" 的缺点,更方便地生成对象,Javascript的国际标准 ECMAScript 第五版(目前通行的是第三版),提出了一个新的方法 Object.create()。
用这个方法,"类" 就是一个 对象,不是 函数。
var Cat = {
name: "大毛",
makeSound: function(){ alert("喵喵喵"); }
};
然后,直接用 Object.create() 生成实例,不需要用到 new。
var cat1 = Object.create(Cat);
alert(cat1.name); // 大毛
cat1.makeSound(); // 喵喵喵
目前,各大浏览器的最新版本(包括IE9)都部署了这个方法。如果遇到老式浏览器,可以用下面的代码自行部署。
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
这种方法比 "构造函数法" 简单,但是不能实现私有属性和私有方法,实例对象之间也不能共享数据,对"类"的模拟不够全面。
极简主义法 ( 推荐的方法 )
荷兰程序员 Gabor de Mooij提出了一种比 Object.create() 更好的新方法,他称这种方法为"极简主义法"(minimalist approach)。也是推荐的方法。
这种方法不使用 this 和 prototype,代码部署起来非常简单,这大概也是它被叫做 "极简主义法" 的原因。首先,它也是用一个对象模拟 "类"。在这个类里面,定义一个构造函数 createNew(),用来生成实例。
var Cat = {
createNew: function(){
// some code here
}
};
然后,在 createNew() 里面,定义一个实例对象,把这个实例对象作为返回值。
var Cat = {
createNew: function(){
var cat = {};
cat.name = "大毛";
cat.makeSound = function(){ alert("喵喵喵"); };
return cat;
}
};
使用的时候,调用 createNew() 方法,就可以得到实例对象。
var cat1 = Cat.createNew();
cat1.makeSound(); // 喵喵喵
这种方法的好处是,容易理解,结构清晰优雅,符合传统的"面向对象编程"的构造,因此可以方便地部署下面的特性。
继承
让一个类继承另一个类,实现起来很方便。只要在前者的 createNew() 方法中,调用后者的createNew() 方法即可。
先定义一个 Animal 类。
var Animal = {
createNew: function(){
var animal = {};
animal.sleep = function(){ alert("睡懒觉"); };
return animal;
}
};
然后,在 Cat 的 createNew() 方法中,调用 Animal 的 createNew() 方法。
var Cat = {
createNew: function(){
var cat = Animal.createNew();
cat.name = "大毛";
cat.makeSound = function(){ alert("喵喵喵"); };
return cat;
}
};
这样得到的 Cat 实例,就会同时继承 Cat类 和 Animal类。
var cat1 = Cat.createNew();
cat1.sleep(); // 睡懒觉
私有属性 和 私有方法
在 createNew() 方法中,只要不是定义在 cat 对象上的方法和属性,都是私有的。
var Cat = {
createNew: function(){
var cat = {};
var sound = "喵喵喵";
cat.makeSound = function(){ alert(sound); };
return cat;
}
};
上例的内部变量 sound,外部无法读取,只有通过 cat 的公有方法 makeSound() 来读取。
var cat1 = Cat.createNew();
alert(cat1.sound); // undefined
数据共享
有时候,我们需要所有实例对象,能够读写同一项内部数据。这个时候,只要把这个内部数据,封装在类对象的里面、createNew()方法的外面即可。
var Cat = {
sound : "喵喵喵",
createNew: function(){
var cat = {};
cat.makeSound = function(){ alert(Cat.sound); };
cat.changeSound = function(x){ Cat.sound = x; };
return cat;
}
};
然后,生成两个实例对象:
var cat1 = Cat.createNew();
var cat2 = Cat.createNew();
cat1.makeSound(); // 喵喵喵
这时,如果有一个实例对象,修改了共享的数据,另一个实例对象也会受到影响。
cat2.changeSound("啦啦啦");
cat1.makeSound(); // 啦啦啦
使用关键字 class
参考资料:
- MDN Classes:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes
- JavaScript 类完整指南:https://zhuanlan.zhihu.com/p/101988767
- JavaScript 类(class):https://www.runoob.com/.js/js-class-intro.html
- JavaScript 中的类:https://zhuanlan.zhihu.com/p/127798747
以前都是通过构造函数 function 和 原型prototype 来实现类的效果,在ES6中新增了 class 关键字用来定义类,使用 class 关键字定义类的写法更加清晰,更像面向对象的语法。但是可以看作是语法糖,因为它还是构造函数和原型的概念。
类声明
定义类有2中方式,类声明 和 类表达式:
// 类声明
class Student {}
// 类表达式
const Student = class {}
ECMAScript 6 中定义一个类示例代码:
class Animal {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hello,${this.name}`);
}
}
Animal.prototype.constructor === Animal; // true
let dog = new Animal('dog');
dog.sayHi(); // hello, dog
constructor 方法用来创建和初始化对象,而且一个类中有且只能有一个consctuctor方法,默认为constructor(){}。dog 就是Animal实例化的对象。
ES5 中创建类
在前面说到,class 只是基于现有的 JavaSript 实现类的语法糖,那先让我们简单使用 ES5 中的方法来模拟类,并实例化。
function Animal(name) {
this.name = name;
}
Animal.prototype.sayHi = function() {
console.log(`hello,${this.name}`);
}
let dog = new Animal('dog');
dog.sayHi(); // hello, dog
可以看出,class 中的 constructor() 方法就相当于 Animal() 构造函数,而在 class 中定义属性就相当于直接在原型对象上定义属性。我们不妨这样试一试:
class Animal {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hello, ${this.name}`);
}
}
let dog = new Animal('dog');
dog.sayHi(); // hello, dog
dog.__proto__ === Animal.prototype; // true
dog.__proto__.sayHi = function() {
console.log(`hi, ${this.name}`);
}
dog.sayHi(); // hi, dog
可以看出 class 还是依靠原型对象来实现类的。
为什么说它是语法糖
因为类实际上它是一个 function,区别在于构造函数是函数作用域,类是块级作用域,类中的方法,都是定义在类的prototype上面,
class Student {
take() {}
}
const a = new Student()
console.log(typeof Student) // function
console.log(a.take === Student.prototype.take) // true
// 同等于
function Student() {}
Student.prototype.take = function() {}
const a = new Student()
console.log(typeof Student) // function
console.log(a.take === Student.prototype.take) // true
类包含的属性和方法
类可以包含 构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。空的类定义照样有效。
class Student {
// 实例属性 也可以放在这
// b = 1
// 静态属性
static a = 1
// 构造函数
constructor() {
// 实例属性 - 也可以放在类的最顶层
this.b = 1
}
// 获取函数
get myName() {}
// 设置函数
set myName() {}
// 静态方法 不会被实例继承
static show() {}
// 方法
take() {}
// 私有方法
_take() {}
}
实例属性必须定义在类方法中:
class Animal {
constructor(name) {
this.name = name; // 实例属性
}
}
静态属性和原型属性避必须在类的外面定义:
Animal.age = 18; // “静态属性”
Animal.prototype.sex = 'male'; // 原型属性
实例属性顾名思义,就是对象独有的方法/属性,静态属性就是位于Class本身的属性,但是ES6中明确说明Class只有静态方法,没有静态属性,原型属性也很容易理解也很容易看出,就是位于原型链上的属性/方法。
类的构造函数
类的构造函数关键字是 constructor,它同等于原型中的 prototype.constructor。
如果没有写 constructor 函数,那么会默认有一个空的 constructor 函数。
class A {
constructor() {
this.name = '小明'
}
}
const b = new A()
b.constructor === A.prototype.constructor // true
当使用 new 操作符创建实例时,会调用 constructor 构造函数。
类的方法
class Student {
// 方法
take() {}
}
类的静态方法
跟类的方法一样,只不过前面加上static关键字。
静态方法不会被实例继承。
父类的静态方法可以被子类继承。
class A {
// 静态方法
static show() {
console.log('hi')
}
}
class B extends A {}
const c = new A()
c.show() // c.show is not a function
B.show() // hi
所有在类中定义的方法会被实例继承,但是有时候我们并不想所有实例都能继承某个方法,这时候,static关键字就能达到你的目的,在声明方法前加上static关键字,这个方法就不会被实例继承,而是直接通过类来调用,它被叫做静态方法,如下:
class Animal {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hello, ${this.name}`);
}
static bark() {
console.log('喵喵喵');
}
}
let dog = new Animal('dog');
dog.bark(); // TypeError
Animal.bark(); // 喵喵喵
静态方法虽然不能被实例继承,但是可以被子类继承,但是子类的实例依旧没有继承它:
class Animal {
static bark() {
console.log('喵喵喵');
}
}
class Dog extends Animal{
}
Dog.bark(); // 喵喵喵
let dog = new Dog();
dog.bark(); // TypeError
类的私有方法
es6中没有提供这个方法,但是通常都是在方法前面加上下划线来表示。
class A {
// 私有方法
_show() {
console.log('hi')
}
}
取值函数(getter)和存值函数(setter)
在类中有 set 和 get 关键词,可以对某个属性设置存值和取值函数,拦截它的存取行为。
class A {
constructor () {
this.name = '小米'
}
get name () {
return 'get'
}
set name (val) {
console.log('set' + val)
}
}
const b = new A()
b.name // get
b.name = 123 // set123
Class 的 继承
class 使用 extends 关键字来创建子类
class Animal {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hello, ${this.name}`);
}
}
class Dog extends Animal {
bark() {
console.log(`喵喵喵`);
}
}
let wangcai = new Dog('旺财');
wangcai.bark(); // 喵喵喵
wangcai.sayHi(); // hello, 旺财
但如果在子类中定义了 constructor 方法,必须先调用 super() 才能使用 this,因为子类并没有 this对象,而是继承父类的 this 对象,所以 super 必须在使用 this 关键字之前使用:
class Animal {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hello, ${this.name}`);
}
}
class Dog extends Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
};
bark() {
console.log(this.sound);
}
}
let wangcai = new Dog('旺财', '喵喵喵');
wangcai.bark(); // referenceError
class Dog extends Animal {
constructor(name, sound) {
super(name);
this.sound = sound;
};
bark() {
console.log(this.sound);
}
}
let wangcai = new Dog('旺财', '喵喵喵');
wangcai.bark(); // 喵喵喵
super 方法
注意如果子类如果没写constructor构造函数,则会默认有constructor构造函数和super方法,但是如果显性的写了constructor构造函数,那么必须在子类的构造函数中添加super方法,添加之后会调用父类的构造函数并得到父类的属性和方法,如果没有添加super方法则会报ReferenceError错误。
super 不仅可以调用父类的 constructor 函数,还可以调用父类上的方法:
class Animal {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hello, ${this.name}`);
}
}
class Dog extends Animal {
bark() {
super.sayHi();
}
}
let wangcai = new Dog('旺财');
wangcai.bark(); // hello, 旺财
示例:
class A {
constructor () {
this.name = '小米'
}
show() {
console.log('hi')
}
}
class B extends A {
constructor () {
super() // 如果不写super,则会报ReferenceError错误
}
}
const c = new B()
super 方法中也可以传参
class A {
constructor (name) {
this.name = name
}
show() {
console.log('hi')
}
}
class B extends A {
constructor () {
super('小红')
}
}
const c = new B()
c.name // 小红
方法中的 this 指向
类的方法中如果有 this,那么它指向的是类的实例。但是如果将它单独拿出来使用那么会报错。
class A {
constructor () {
this.name = '小米'
}
show () {
console.log(this.name)
}
}
const b = new A()
b.show() // 小米
const { show } = b // Cannot read property 'name' of undefined
解决办法有2种:
- 在构造函数中绑定 this
class A {
constructor () {
this.name = '小米'
this.show = this.show.bind(this)
}
show () {
console.log(this.name)
}
}
- 使用箭头函数
class A {
constructor () {
this.name = '小米'
this.show = () => this
}
show () {
console.log(this.name)
}
}
区分是否继承了这个类
区分是否继承了这个类使用Object.getPrototypeOf函数。
class A {
constructor () {
this.name = '小米'
}
show() {
console.log('hi')
}
}
class B extends A {
constructor () {
super()
}
}
class C {}
Object.getPrototypeOf(B) === A // true 是继承的A类
Object.getPrototypeOf(B) === C // false 没有继承C类
详解 Object 对象
详解 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
构造函数 和 对象字面量 这两种方法有一个缺点就是:如果要创建多个对象,写起来很繁琐,所以后来就有了一种创建自定义构造函数的方法来创建对象,如下所示:
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person("Jack", 15);
这种方式可以很方便的创建多个同样的对象,也是目前比较常用的方法。
javascript 中 function(){}(), new function(), new Function(), Function
javascript 中的类的构造:
javascript 中有对象的概念,却没有类的概念。
- 类 是一种抽象的概念,例如:动物、植物;
- 对象 则是指这种概念中的实体,比如 "中国人、美国人、杨树、柳树";
- 实例化 就是指以类为基础构建一个实体。
类所拥有的特征,其实例化对象,也一定拥有这些特征,而且实例化后可能拥有更多特征。
javascript 在用到对象时,完全没有类的概念,但是编程的世界里,无奇不有,可以通过 function 构造出一种假想的类,从而实现 javascript 中类的构造。比如,我们通过下面的方法来构造一个类:
//java
class Book {
private String name;
private double price;
public Book(name,price) {this.name=name;this.price=price;}
public void setName(String name) { this.name = name;}
public void setPrice(double price) {this.price = price;}
public String getInfo() {...}
}
Book book1 = new Book('java',13.3);
//javascript
function Book(name,price) {
this.name = name;
this.price = price;
this.setName = function(name) {this.name = name;};
this.setPrice = function(price) {this.price = price};
this.getInfo = function() {return this.name + ' ' + this.price;};
}
var book1 = new Book('java',13.3);
function(){}() 让变量快速初始化结果
在《javascript立即执行某个函数:插件中function(){}()再思考》一文中,我详细阐述了 function(){}() 的作用及理解思路。这里不再赘述,现在,我们面临的新问题是,知道了它的作用,我们如何使用它?让我们来看一段代码:
var timestamp = function(){
var timestamp = Date.parse(new Date());
return timestamp/1000;
}();
当我们要使用一个变量时,我们希望这个变量在一个环节完成我们的赋值,使用上面的这种方法,可以减少代码上下文执行逻辑,如果按照我们以前的方法,代码可能会写成:
var timestamp = Date.parse(new Data());
timestamp = timestamp/1000;
看上去好像比上面的操作简洁多了,只需要两行代码。但是我们仔细去观察,就会发现第一段代码其实本身仅是一个赋值操作,在 function 中完成的所有动作将会在function执行完后全部释放,整个代码看上去好像只执行了一条语句一样。
而实际上更重要的意义在于它可以让一个变量在初始化时,就具备了运算结果的效果。
使用 new function 初始化一个可操作对象
上面讲了 javascript 中的类,而使用 new function 可以实例化这个类。但是我们实际上有的时候在为一个变量赋值的时候,希望直接将它初始化为一个可操作的对象,比如像这样:
// 这里的数据库操作是我虚拟出来的一种数据库操作形式
var $db = new function(){
var $db = db_connect('127.0.0.1','root','');
$db.use('database');
this.select = function(table,where) {
var result = $db.query('select from ' + table + ' where ' + where);
return $db.fetchAll(result);
}
};
当我们要对数据库 database 进行查询时,只需要通过 var list = $db.select('table','1=1');
进行操作即可,数据库的初始化结果已经在$db这个变量中了。
Function 是由 function 关键字定义的 函数对象的原型
在 javascript 中,多出了一个原型的概念。所谓原型,其实就是一个对象的本质( 可以理解成 基类 ),但复杂就复杂在,原型本身也是对象,因此,任何一个对象又可以作为其他对象的原型。Function 就相当于一个系统原型,可以把它理解为一种 "基本对象类型",是 "对象" 这个概念范畴类的基本数据类型。
除了 Function 之外,其实还有很多类似的首字母大写的对象原型,例如 Object, Array, Image 等等。有一种说法是:javascript 中所有的一切都是对象(除了基本数据类型,其他的一切全是对象),所有的对象都是 Object 衍生出来的。(按照这种说法,我们应该返回去再思考,上面说的类的假设是否成立。)
极其重要的 prototype 概念
prototype 的概念在 javascript 中极其重要,它是 javascript 中完成上面说的 "一切皆对象" 的关键。有了prototype,才有了原型,有了原型,才有了 javascript 五彩缤纷的世界(当然,也有人说是杂乱的)。我们可以这样去理解 prototype:世界上本没有 javascript,上帝说要有Object,于是有了 Object,可是要有 Function 怎么办?只需要对 Object 进行扩展,可是如何扩展?只需要用prototype……当然,这是乱扯的,不过在 javascript 中,只要是 function,就一定会有一个prototype 属性。实际上确实是这样
Function.prototype.show = function() {...}
在原型的基础上通过prototype新增属性或方法,则以该对象为原型的实例化对象中,必然存在新增的属性或方法,而且它的内容是静态不可重载的。原型之所以被称为原型,可能正是因为这种不可重载的特质。
比如上面的这段代码,会导致每一个实例化的function,都会具备一个show方法。而如果我们自己创建了一个类,则可以通过 prototype 将之转化为原型:
function Cat() {...}
Cat.prototype.run = function() {};
var cat1 = new Cat();
这时,对于 cat1 而言,Cat 就是原型,而该原型拥有一个 run 的原始方法,所以无论实例化多少个 Cat,每一个实例化对象都有 run 方法,而且该方法是不能被重载的,通过 cat1.run = function(){} 是无效的。
为了和其他语言的类的定义方法统一,我们可以将这种原型属性在定义类的时候,写在类的构造里面:
function Cat() {
....
Cat.prototype.run = function() {};
}
new Function() 是函数原型的一个实例化
在理解了 Function 原型的概念之后,再来看 new Function()就显得很容易了。首先来看下我们是怎么使用这种奇特的写法的:
var message = new Function('msg','alert(msg)');
// 等价于:
function message(msg) {
alert(msg);
}
new Function(参数1,参数2,…,参数n,函数体),它的本意其实是通过实例化一个Function原型,得到一个数据类型为function的对象,也就是一个函数,而该变量就是函数名。
this 在这类 function 中的指向
this 在 javascript 中真的是无法让我们捉摸透彻。但是有一个小窍门,就是:一般情况下,this 指向的是当前实例化对象,如果没有找到该对象,则是指向 window。从使用上来讲,我们应该排除new Function 的讨论,因为它和我们常用的函数声明是一致的。
- 普通的函数中this的指向:函数声明的时候,如果使用了this,那么就要看是把该函数当做一个对象加以返回,还是以仅执行函数体。普通函数执行时,我们完全没有引入对象、类这些概念,因此,this 指向 window。通过代码来看下:
var msg; function message(msg) { this.msg = msg; } message('ok'); alert(msg);
首先是声明一个函数message,在函数中this.msg实际上就是window.msg,也实际上就是代码开头的msg。因此,当执行完message(‘ok’)的时候,开头的全局变量msg也被赋值为ok。
-
通过 function 构造类时 this 的指向:如果function被构造为一个类,那么必然存在该类被实例化的一个过程,如果没有实例化,那么该类实际上并没有在程序中被使用。而一旦实例化,那么this将指向实例化的对象。
var age = 3; var cat1 = new function() { this.name = 'Tom'; this.age = 2; this.weight = function(age) { var age = age * 2; var _age = this.age * 2; return 'weight by age:' + age + '; weight by this.age:' + _age; }(this.age); this.eye = new function() { this.size = '1.5cm'; this.color = 'red'; }; this.catching = function(mouse) { return this.name + ' is catching ' + mouse; }; }; alert(cat1.weight); alert(cat1.eye.color); alert(cat1.catching('Jerry'));
上面代码中标记了4处红色的this的使用。根据我们的原则,this指向实例化对象,我们来对每一个 this 进行分解。
首先是 cat1.weight,我使用了 function(){}(),直接利用猫咪的年龄进行计算得出体重返回给weight属性。
第一个 this.age 出现在function(){}(this.age),这个this.age实际上是一个传值过程,如果你对我之前分析function(){}()比较了解的话,应该知道,this.age实际上是和前面this.age = 2指同一个,这里的this.age的this,首先要去找它所在的function,然后看这个function是否被实例化,最后确认,确实被实例化为cat1,因此this=cat1。
第二个 this.age 出现在function(){this.age}()。同样,你先需要对function(){}()再次深入了解,实际上,function(){}()就是执行一个函数而已,我们前面提到了,普通函数执行中this=window,所以,这里的this.age实际上是var age = 3。
第三个 this.color 出现在new function(){this.color},这里就比较好玩,由于有一个new,实际上也被实例化了,只不过是对匿名类的实例化,没有类名,而且实例化仅可能出现这一次。因此,this.color的this要去找new function的主人,也就是this.eye,而this.eye的this=cat1,所以cat1.eye.color=’red’。
第四个 this.name 出现在function(){this.name},它出现在cacthing方法中,它既不是普通的函数执行,也不是实例化为对象,而是正常的类中的方法的声明,因此this指向要去找它所在的function被实例化的对象,也就是cat1。
对象实例 的 属性和方法
不管通过哪种方式创建了对象实例后,该 实例 都会拥有 下面的属性和方法,下面将会逐个说明。
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,然后通过 keys 和 getOwnPropertyNames 两个方法来获取属性列表,能明显看出了两者区别。
属性 特性型 方法
这个主要是前面提到的三个方法:
- 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可以用来判断一个对象是否被冻结了。
可以发现:这三个限制对象的方法的限制程度是依次上升的。
Object 常用方法总结
参考:Javascript Object常用方法总结 - fozero - 博客园
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.getOwnPropertyNames 和 Object.keys 区别
Object.getOwnPropertyNames 和 Object.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():es6 javascript对象方法Object.assign()_现在学习也不晚-CSDN博客_object.assign
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):运算符 -- JavaScript 标准参考教程(alpha)
JavaScript 中 === 和 == 的区别 ( 参考:Javascript 中 == 和 === 区别是什么? - 知乎 ):
- "===" 叫做 恒等 运算符。( 即 类型和值 都相等 )( 其实叫 全等运算符 更合适。即内存中每个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 // truefalse == 'false' // false
false == 0 // truefalse == 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);
prototype 和 __proto__ 区别 :
- 函数(Function)才有 prototype 属性,
- 对象(除Object)拥有__proto__。
js 原型链 prototype __proto__:https://www.cnblogs.com/mengfangui/p/9566114.html
var a = {}; // 定义一个对象
console.log(a.prototype); //undefined
console.log(a.__proto__); //Object {}
var b = function() {} // 定义一个函数
console.log(b.prototype); //b {}
console.log(b.__proto__); //function() {}
__proto__ 指向
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>__proto__指向</title>
</head>
<body>
<script src="https://cdn.bootcss.com/lodash.js/4.17.10/lodash.min.js"></script>
<script type="text/javascript">
/*1、字面量方式*/
var a = {};
console.log(a.__proto__); //Object {}
console.log(a.__proto__ === a.constructor.prototype); //true
/*2、构造器方式*/
var A = function() {};
var b = new A();
console.log(b.__proto__); //A {}
console.log(b.__proto__ === b.constructor.prototype); //true
/*3、Object.create()方式*/
var a1 = {
a: 1
}
var a2 = Object.create(a1);
console.log(a2.__proto__); //Object {a: 1}
console.log(a2.__proto__ === a2.constructor.prototype); //false(此处即为图1中的例外情况)
</script>
</body>
</html>
__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 Person(){}; // 定义一个 构造函数
var pp = new Person(); // 定义一个 构造函数的实例pp.__proto__ === Person.prototype; // true
pp.constructor === Person; // truepp.__proto__.__proto__ === Person.prototype.__proto__ // true
Person.constructor === Function; // true
Person.prototype.constructor === Person; // true
Person.__proto__ === Function.prototype; //trueFunction.constructor === Function; // true
Function.prototype //ƒ () { [native code] }
Function.__proto__ //ƒ () { [native code] }
Function.__proto__ === Function.prototype; // truefunction a(){}
a.constructor === Function.constructor // true构造函数 的 constructor 指向 构造函数自身
继续深入,来看:
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)
综上我们已经得出:
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
首先要清楚:JS中所有事物都是对象,对象是拥有属性和方法的数据。所以函数也是对象
当创建一个函数的时候,发生了什么?
实际上,
- 函数是 Function类型的实例,此时可以把每一个创建出来的函数,当成是Function类型的实例对象,
- 所以函数本身拥有的对象属性,来源于Function,即 Fn.Constructor 就是 Function
- 但是与此同时要注意:Function.prototype.__proto__ === Object.prototype 可以理解为:构造器函数的构造函数是Object 也可以简单的理解:函数即对象
了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲 实例 和 原型 的关系:
定义对象的两种方式:
class Cat {
constructor(){}
toString(){}
toValue(){}
}
等价于
function Cat(){}
Cat.prototype = {
constructor(){}
toString(){}
toValue(){}
}
原型链中的继承
function Parent(){
this.name = "Parent";
this.sex = "boy"
}
function Child(){
this.name = "Child";
}
Child.prototype = new Parent()
child = new Child()
child.sex
实例 与 原型
当读取 实例的属性 时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
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多态的详细解释:🍃【何不三连】JS面向对象最后一弹-多态篇(羽化升仙) - 掘金
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版》
2、JavaScript 逆向技巧
From:https://www.cnblogs.com/Renyi-Fan/p/12650448.html
2.1 总结
1. 全局关键词搜索
2. 事件监听断点
3. 堆栈跟踪
非常频繁使用的事件监听断点:script、XHR
一般频繁使用的:Dom断点、Control、timer
不太常用但是偶尔会用到的:Mouse
一句话总结:
- 1. 搜索:全局搜索、代码内搜索
- 2. debug:常规 debug、XHR debug、行为 debug
- 3. 查看请求调用的堆栈
- 4. 执行 堆内存 中的函数
- 5. 修改 堆栈 中的参数值
- 6. 写 js 代码
- 7. 打印 windows 对象的值
- 8. 勾子:cookie钩子、请求钩子、header钩子
2.2 js 逆向技巧
所需环境:Chrome浏览器
1. 搜索
1.1 全局搜索 ( Ctrl + shift + f )
适用于根据关键词快速定位关键文件及代码
当前页面 右键 ---> 检查,弹出检查工具
搜索支持 关键词、正则表达式
1.2 代码内搜索 ( Ctrl + f )
适用于根据关键词快速定位关键代码
点击代码,然后按 ctrl+f 或 command+f 调出搜索框。搜索支持 关键词、css表达式、xpath
1.3 搜索 "HTTP头和响应"
在 "所有资源的HTTP头和响应" 中搜索某个字符串或正则表达式
分析 发起 调用
上面的搜索是其中一种查找人口的方式,这是从源码级别上直接查找。也可以通过其他的思路来查找入口,比如可以查看发起调用的流程。可以直接从 Network 请求里面的 Initiator 查看当前请求构造的相关逻辑,
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插件、油猴脚本 )
以 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 ( 开发chrome插件,然后从本地加载 )
插件的配置文件
{
"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
开发 油猴脚本,通过 油猴(Tampermonkey) 进行 hook
油猴脚本开发:https://juejin.cn/post/7022654292880424991
快速上手油猴插件开发(实战篇):https://juejin.cn/post/6925605904561750030/
9. 模拟执行
Python 执行 js
由于Python简单易用,同时也能够模拟调用执行JavaScript。如果整体逻辑不复杂的话,可以尝试使用 Python 来把整个加密流程完整实现一遍。如果整体流程相对复杂,可以尝试Python执行JavaScript。
JavaScript 模拟执行 + API
由于整个逻辑是 JavaScript 实现的,使用 Python 来执行 JavaScript 难免会有一些不太方便的地方,而 Nodejs 天生就有对 JavaScript 的支持。为了更通用地实现 JavaScript 的模拟调用,可以用express 来模拟调用 JavaScript,同时将其暴露成一个 API,从而实现跨语言的调用。
浏览器模拟执行
由于整个逻辑是运行在浏览器里面的,当然也可以将浏览器当作整个执行环境。比如使用 Selenium、PlayWright 等来尝试执行一些JavaScript代码得到一些返回结果。
2.3 js 相关反爬
刷新型 cookie 反爬
瑞数
加固乐
调试干扰 (无限debugger、控制台检测、内存爆破)
无限 debugger
无限 debugger,还有一个非常靓丽的名字:debugger 地狱
最关键的一点:无限 debugger 不可能无限,否则浏览器会卡死
实现debugger的方案: Function,关键字,eval制作虚拟机
【不可混淆】
1. debugger;【可混淆】
2. eval("debugger;")【可重度混淆】
3. Function("debugger").call()/apply() 或赋值 bind()
XXX.constructor("debugger").call("action")
Function.constructor("debugger").call("action")
(function() {return !![];}["constructor"]("debugger")["call"]("action"))无限 debugger 的处理实际上很简单,有以下几种情况
1. 无限 debugger 贯穿全局:干掉定时器等全局事件(置空或重写)
2. 无限 debugger 在加密逻辑之前。想要调试到函数入口,必须越过这个无限 debugger
- 针对静态文件/伪动态文件(大部分都是这个情况)
- 用 fiddler Autoresponse 删掉 debugger
- 可以右键 never
- 针对真动态文件或 Autoresponse 失效或删掉 debugger 逻辑很繁琐的情况下
1. 如果是 Function 原理的 debugger,可以重写 函数构造器2. 如果是 eval 型的构造器,可以重构 eval 函数Function.prototype.constructor_bak = Function.prototype.constructor; Function.prototype.constructor = function(){ //var arg_string = ""; //for (var i=0; i<arguments.length; i++){ arg_string += arguments[i] } if("debugger" === arguments){ }else{ return Function.prototype.constructor_bak.apply(this, arguments); } };
3. 如果是定时器,并且 2失效了,可以重构定时器eval_bak = eval window.eval = function(a){ if('debugger'===a){} else{eval_bak(a);} }
4. 在以上方式都失效时,向上找堆栈,在进入无限debugger之前打上断点将触发无限debugger 的函数置空(最麻烦,但是适用性最广)3. 无限 debugger 在加密逻辑之后:不用管,script/ 第一行断点打上,从头开始
控制台 检测
:https://www.cnblogs.com/wuxianyu/p/14523102.html
:https://match.yuanrenxue.com/match/16
打个 script 断点,当跳转到主页时,查看 源码,代码如下:
<script> var ConsoleManager={ onOpen:function(){ alert("Console is opened") }, onClose:function(){ alert("Console is closed") }, init:function(){ var self = this; var x = document.createElement('div'); var isOpening = false,isOpened=false; Object.defineProperty(x, 'id', { get:function(){ if(!isOpening){ self.onOpen(); isOpening=true; } isOpened=true; } }); setInterval(function(){ isOpened=false; console.info(x); console.clear(); if(!isOpened && isOpening){ self.onClose(); isOpening=false; } },200) } } ConsoleManager.onOpen = function(){ try{ window.open('http://match.yuanrenxue.com/',target='_self'); }catch(err){ var a = document.createElement("button"); a.onclick=function(){ window.open('http://match.yuanrenxue.com/',target='_self'); }; a.click(); } }; ConsoleManager.init(); </script>
上面过控制台检测
e.open = function(){} ConsoleManager.onOpen = function(){} // 或者 window.open = function(){} setInterval = function(){}
打上断点之后,当要运行跳转时,直接把跳转 置为 空函数 即可
控制台检测原理:原理就是使用 console 的特性,只有在控制台打开的时候,console 才会对一些信息和内容进行打印。如果设置一个定时器,在定时器中不断循环获取一个参数x,并且对这个参数x进行 hook,利用 Object.defineProperty 处理其get 属性,那么当打开控制台的一瞬间,console 就会生效,获取属性并触发 hook,执行 Object.defineProperty 内的逻辑。
chrome 浏览器有这个特性, firefox 没有这个特性,可以多换几个浏览器试一下。
<html> <head></head> <script> var x = document.createElement('div'); Object.defineProperty(x, 'id',{ get:function(){ alert(12345)' } }); function test(){ console.log(x); console.clear(); } setInterval(test, 200); </script> </html>
内存爆破原理
其实可以叫做 "蜜罐内存爆破",当检测出来时( 例如:js 代码进行了格式化,导致特征字符穿不匹配,使用 js 正则检测出来 ),走到蜜罐里面一直循环。
对于 js 格式化被正则检测出来,可以分段进行 js 格式化,逐渐排出,找到关键位置,然后让正则检测时返回 true
内存爆破,指 js 通过死循环/频繁操作数据库( 包括cookie ) / 频繁调取 history 等方式,使浏览器崩溃的一种反调试手段。
关键词:频繁
还有一种特性情况:js文件很大,电脑内存不足(这种情况在调试层面几乎无解)
特点:
1. 正常运行时,一切正常
2. 调试时利用时间差,调试特点等讲控制流推入循环
3. ob混淆内存爆破原理:利用正则/toString() 判断代码是否进行格式化
4. 利用浏览器指纹判断是否为浏览器环境
2.4 逆向 抓取 实战
示例网站:https://spa6.scrape.center/
代码:https://github.com/Python3WebSpider/ScrapeSpa6
分析请求
发现 Ajax 请求接口和每部电影的 URL 都包含了加密参数,不仅在API参数有加密,而且前端JavaScript 也有压缩和混淆,前端压缩打包工具使用webpack,混淆工具使用 javascript-obfuscator。分析该网站需要熟练掌握浏览器的开发者工具和一定的调试技巧,另外还需要用到一些 Hook 技术等辅助分析手段。
点击任意一部电影,观察一下 URL 的变化 可以看到详情页的 URL 包含了一个长字符串,看上去像是 Base64 编码的内容。
再看 Ajax 请求,从列表页的第1页到第 10 页依次点一下,观察 Ajax 请求可以看到 Aiax 接口的URL里多了一个 token,而且在不同的页码,token 是不一样的,它们同样看似是 Base64 编码的字符串。另外,这个接口还有时效性。如果我们把 Ajax 接口的 URL直接复制下来,短期内是可以访问的,但是过段时间之后就无法访问了,会直接返回401状态码。
总结:
- 列表页的 Ajax 接口参数带有加密的 token;
- 详情页的 URL 带有加密 id;
- 详情页的 Ajax 接口参数带有加密 id 和加密 token。
如果要想通过接口的形式进行爬取,必须构造出加密的 id 和 token。由于是网页,所以其加密逻辑一定藏在前端代码里,但是前端为了保护其接口加密逻辑不被轻易分析出来,会采取压缩、混淆等方式来加大分析的难度。下面就分析下这个网站的源代码和JavaScript文件是怎样的。
查看 混淆的 js
首先看网站的源代码,在网站上点击鼠标右键,然后点击 "查看源代码" 选项,或者 Ctrl + u 快捷键 查看
这是一个典型的 SPA(单页Web应用)页面,其JavaScript文件名带有编码字符、chunk、vendors 等关键字,这就是经过 webpack 打包压缩后的源代码,目前主流的前端开发框架 Vue.js、React.js 等的输出结果都是类似这样的。
随便点开一个 js 文件
寻找列表页 Ajax 入口
简单介绍两种寻找入口的方式
- 全局搜索标志字符串
url:https://spa6.scrape.center/api/movie?limit=10&offset=0&token=YjFjMDUwYWIxN2JhMjM2MzQwN2M3MDI5YmE4ZGZlOTViMjAyMWMwOCwxNjk3MDA2OTcy
可以看到请求的url中有 limit、offset、token 关键字,直接搜索
这里的字符串 token 并没有被混淆,可以非常容易成为寻找入口的依据,所以这样的字符串也会被混淆成类似Unicode、Base64、RC4等的编码形式,这样就没法轻松搜索到了
这时就可以XHR 断点,利用该方法我们可以方便地找到发起Aiax请求的一些入口位置。
- 设置Aiax断点
手动设置断点,并删除 XHR 断点,
然后刷新页面,会自动断到 设置的断点位置
选中 要查看的代码 ,会出现浮动窗口,点击 js 文件,跳转到对应的代码
加密逻辑:
- 将/api/movie放到一个列表里
- 在列表中加人当前时间戳
- 将列表内容用逗号拼接:
- 将拼接的结果进行SHA1编码;
- 将编码的结果和时间戳再次拼接
- 将拼接后的结果进行Base64编码
验证一下,如果逻辑没问题,就可以用 Python 来实现
怎么去注人这个代码呢? 这里介绍3种注入方法
- 控制台注入。对于控制台直接输入的 Hook 代码,页面刷新就无效了。但是这个网站是SPA页面,点击详情页的时候页面是不会整个刷新的,因此这段代码依然生效。如果不是SPA页面即每次访问都需要刷新页面的网站,那么这种注入方式就不生效了。
- 重写 JavaScript。借助 Chrome 浏览器的 Overrides 功能,可以实现某些 JavaScript 文件的重写和保存。Overrides 会在本地生成一个JavaScript 文件副本,以后每次刷新,都会使用副本的内容。操作步骤:切换到Sources 面板中的Overrides选项卡,然后选择一个文件夹,比如自定义一个ChromeOverrides 文件夹,然后随便选一个 JavaScript 脚本,在最后面添加下面的注人脚本。保存文件,此时可能提示页面崩溃,不用担心,重新刷新页面就好了。可以发现,现在浏览器加载的JavaScript文件就是修改过的js,文件名左侧会有一个圆点标识符。
- Tampermonkey 注人
hook base64
(function (){
'use strict'
function hook(object, attr){
let func = object[attr];
object[attr] = function () {
console.log('hooked', object, attr, arguments);
let ret = func.apply(object, arguments);
debugger;
console.log('result', ret);
return ret;
}
}
// hook base64
hook(window, 'btoa');
})()
Tampermonkey 注入
// ==UserScript==
// @name HookBase64
// @namespace https://scrape.center/
// @version 0.1
// @description Hook Base64 encode function
// @author Germey
// @match https://spa6.scrape.center/
// @grant none
// @run-at document-start
// ==UserScript==
(function (){
'use strict'
function hook(object, attr){
let func = object[attr];
object[attr] = function () {
console.log('hooked', object, attr, arguments);
let ret = func.apply(object, arguments);
debugger;
console.log('result', ret);
return ret;
}
}
// hook base64
hook(window, 'btoa');
})()
代码
import hashlib
import time
import base64
from typing import List, Any
import requests
INDEX_URL = 'https://dynamic6.scrape.cuiqingcai.com/api/movie?limit={limit}&offset={offset}&token={token}'
DETAIL_URL = 'https://dynamic6.scrape.cuiqingcai.com/api/movie/{id}?token={token}'
LIMIT = 10
OFFSET = 0
SECRET = 'ef34#teuq0btua#(-57w1q5o5--j@98xygimlyfxs*-!i-0-mb'
def get_token(args: List[Any]):
timestamp = str(int(time.time()))
args.append(timestamp)
sign = hashlib.sha1(','.join(args).encode('utf-8')).hexdigest()
return base64.b64encode(','.join([sign, timestamp]).encode('utf-8')).decode('utf-8')
args = ['/api/movie']
token = get_token(args=args)
index_url = INDEX_URL.format(limit=LIMIT, offset=OFFSET, token=token)
response = requests.get(index_url)
print('response', response.json())
result = response.json()
for item in result['results']:
id = item['id']
encrypt_id = base64.b64encode((SECRET + str(id)).encode('utf-8')).decode('utf-8')
args = [f'/api/movie/{encrypt_id}']
token = get_token(args=args)
detail_url = DETAIL_URL.format(id=encrypt_id, token=token)
response = requests.get(detail_url)
print('response', response.json())