1、JavaScript 基础
菜鸟教程 JavaScript 教程:https://www.runoob.com/js/js-tutorial.html
- 1. 基础数据类型:number、string、boolean、null、undefined、object
- 2. 顺序、条件、循环、比较
- 3. 函数
- 4. 运算符
js 数组遍历方法
JavaScript 作用域:
强烈推荐书籍《JavaScript高级程序设计 第4版》:https://book.douban.com/subject/35175321/
示例:
1.1 Javascript 函数
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023021053637728
JavaScript 中的函数是`头等公民`,不仅可以像变量一样使用它,同时它还具有十分强大的抽象能力;
定义函数的 2 种方式
在JavaScript 中,定义函数的方式如下:
上述abs()
函数的定义如下:
-
function
指出这是一个函数定义; -
abs
是函数的名称; -
(x)
括号内列出函数的参数,多个参数以,
分隔; -
{ ... }
之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。
请注意,函数体内部的语句在执行时,一旦执行到return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为undefined
。
由于JavaScript的函数也是一个对象,上述定义的abs()
函数实际上是一个函数对象,而函数名abs
可以视为指向该函数的变量。
因此,第二种定义函数的方式如下:
在这种方式下,function (x) { ... }
是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量abs
,所以,通过变量abs
就可以调用该函数。
注意:上述两种定义完全等价,第二种方式按照完整语法需要在函数体末尾加一个
;
,表示赋值语句结束。( 加不加都一样,如果按照语法完整性要求,需要加上 )
调用函数
调用函数时,按顺序传入参数即可:
由于JavaScript 允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
传入的参数比定义的少也没有问题:
此时 abs(x)
函数的参数x
将收到undefined
,计算结果为NaN
。要避免收到 undefined
,可以对参数进行检查:
arguments
JavaScript 还有一个免费赠送的关键字arguments
,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments
类似 Array
但它不是一个Array
:
利用arguments
,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值,实际上arguments
最常用于判断传入参数的个数。
rest 参数
由于 JavaScrip t函数允许接收任意个参数,于是我们就不得不用arguments
来获取所有参数:
为了获取除了已定义参数a
、b
之外的参数,我们不得不用arguments
,并且循环要从索引2
开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的rest
参数,有没有更好的方法?
ES6 标准引入了rest参数,上面的函数可以改写为:
rest参数只能写在最后,前面用...
标识,从运行结果可知,传入的参数先绑定a
、b
,多余的参数以数组形式交给变量rest
,所以,不再需要arguments
我们就获取了全部参数。
如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined
)。
因为rest参数是ES6新标准,所以你需要测试一下浏览器是否支持。请用rest参数编写一个sum()
函数,接收任意个参数并返回它们的和:
测试:
小心你的 return 语句
JavaScript 引擎有一个在行末自动添加分号的机制,
这可能让你栽到return语句的一个大坑。。。
示例:
如果把 return 语句拆成两行:
要小心了,由于 JavaScript 引擎在行末自动添加分号的机制,上面的代码实际上变成了:
所以正确的多行写法是:
变量作用域与解构赋值
在 JavaScript 中,用var声明
的变量实际上是有作用域的。
如果一个变量在函数体内部声明
,则该变量的作用域为整个函数体,在函数体外不可引用该变量:
如果两个不同的函数各自声明
了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响:
由于 JavaScript 的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行:
如果内部函数和外部函数的变量名重名怎么办?来测试一下:
这说明 JavaScript 的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。
变量提升
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:
虽然是strict模式,但语句var x = 'Hello, ' + y;
并不报错,原因是变量y
在稍后申明了。但是console.log
显示Hello, undefined
,说明变量y
的值为undefined
。这正是因为JavaScript引擎自动提升了变量y
的声明,但不会提升变量y
的赋值。
对于上述foo()
函数,JavaScript引擎看到的代码相当于:
由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var
申明函数内部用到的所有变量:
全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript 默认有一个全局对象 window
,全局作用域的变量实际上被绑定到 window
的一个属性。 在 nodejs 中,是绑定到 global 这个变量中
因此,直接访问全局变量course
和访问window.course
是完全一样的。
你可能猜到了,由于函数定义有两种方式,以变量方式var foo = function () {}
定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到window
对象:
进一步大胆地猜测,我们每次直接调用的alert()
函数其实也是window
的一个变量:
恢复 alert
这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError
错误。
名字空间
全局变量会绑定到window
上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
把自己的代码全部放入唯一的名字空间MYAPP
中,会大大减少全局变量冲突的可能。
许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。
局部作用域
由于JavaScript的变量作用域实际上是函数内部,我们在for
循环等语句块中是无法定义具有局部作用域的变量的:
为了解决块级作用域,ES6引入了新的关键字let
,用let
替代var
可以申明一个块级作用域的变量:
常量
由于var
和let
申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:
ES6标准引入了新的关键字const
来定义常量,const
与let
都具有块级作用域:
解构赋值
从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。
什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:
现在,在ES6中,可以使用解构赋值,直接对多个变量同时赋值:
注意,对数组元素进行解构赋值时,多个变量要用[...]
括起来。
如果数组本身还有嵌套,也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致:
解构赋值还可以忽略某些元素:
如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:
对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的:
使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined
,这和引用一个不存在的属性获得undefined
是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:
解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined
的问题:
有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:
这是因为JavaScript引擎把{
开头的语句当作了块处理,于是=
不再合法。解决方法是用小括号括起来:
使用场景
解构赋值在很多时候可以大大简化代码。例如,交换两个变量x
和y
的值,可以这么写,不再需要临时变量:
快速获取当前页面的域名和路径:
如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个Date
对象:
它的方便之处在于传入的对象只需要year
、month
和day
这三个属性:
也可以传入hour
、minute
和second
属性:
使用解构赋值可以减少代码量,但是,需要在支持ES6解构赋值特性的现代浏览器中才能正常运行。目前支持解构赋值的浏览器包括Chrome,Firefox,Edge等。
补充几个两数交换的方法
方法( 对象中的函数叫做方法
在一个对象中绑定函数,称为这个对象的方法。
在JavaScript中,对象的定义是这样的:
但是,如果我们给xiaoming
绑定一个函数,就可以做更多的事情。比如,写个age()
方法,返回xiaoming
的年龄:
绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个this
关键字,这个东东是什么?
在一个方法内部,this
是一个特殊变量,它始终指向当前对象,也就是xiaoming
这个变量。所以,this.birth
可以拿到xiaoming
的birth
属性。
让我们拆开写:
单独调用函数getAge()
怎么返回了NaN
?请注意,我们已经进入到了JavaScript的一个大坑里。
JavaScript的函数内部如果调用了this
,那么这个this
到底指向谁?
答案是,视情况而定!
如果以对象的方法形式调用,比如xiaoming.age()
,该函数的this
指向被调用的对象,也就是xiaoming
,这是符合我们预期的。
如果单独调用函数,比如getAge()
,此时,该函数的this
指向全局对象,也就是window
。
坑爹啊!
更坑爹的是,如果这么写:
也是不行的!要保证this
指向正确,必须用obj.xxx()
的形式调用!
由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的this
指向undefined
,因此,在strict模式下,你会得到一个错误:
这个决定只是让错误及时暴露出来,并没有解决this
应该指向的正确位置。
有些时候,喜欢重构的你把方法重构了一下:
结果又报错了!原因是this
指针只在age
方法的函数内指向xiaoming
,在函数内部定义的函数,this
又指向undefined
了!(在非strict模式下,它重新指向全局对象window
!)
修复的办法也不是没有,我们用一个that
变量首先捕获this
:
用var that = this;
,你就可以放心地在方法内部定义其他函数,而不是把所有语句都堆到一个方法中。
apply
虽然在一个独立的函数调用中,根据是否是strict模式,this
指向undefined
或window
,不过,我们还是可以控制this
的指向的!
要指定函数的this
指向哪个对象,可以用函数本身的apply
方法,它接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
用apply
修复getAge()
调用:
另一个与apply()
类似的方法是call()
,唯一区别是:
-
apply()
把参数打包成Array
再传入; -
call()
把参数按顺序传入。
比如调用Math.max(3, 5, 4)
,分别用apply()
和call()
实现如下:
对普通函数调用,我们通常把this
绑定为null
。
装饰器
利用apply()
,我们还可以动态改变函数的行为。
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
现在假定我们想统计一下代码一共调用了多少次parseInt()
,可以把所有的调用都找出来,然后手动加上count += 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()
:
高阶函数
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023021271742944
高阶函数英文叫Higher-order function。那么什么是高阶函数?
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
当我们调用add(-5, 6, Math.abs)
时,参数x
,y
和f
分别接收-5
,6
和函数Math.abs
,根据函数定义,我们可以推导计算过程为:
map / reduce
map()
方法定义在JavaScript的Array
中,我们调用Array
的map()
方法,传入我们自己的函数,就得到了一个新的Array
作为结果:
filter
用于把Array
的某些元素过滤掉,然后返回剩下的元素。
和map()
类似,Array
的filter()
也接收一个函数。和map()
不同的是,filter()
把传入的函数依次作用于每个元素,然后根据返回值是true
还是false
决定保留还是丢弃该元素。
例如,在一个Array
中,删掉偶数,只保留奇数,可以这么写:
把一个Array
中的空字符串删掉,可以这么写:
可见用filter()
这个高阶函数,关键在于正确实现一个“筛选”函数。
回调函数
filter()
接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示Array
的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身:
利用filter
,可以巧妙地去除Array
的重复元素:
去除重复元素依靠的是indexOf
总是返回第一个元素的位置,后续的重复元素位置与indexOf
返回的位置不相等,因此被filter
滤掉了。
Array
对象的其他高阶函数
对于数组,除了map()
、reduce
、filter()
、sort()
这些方法可以传入一个函数外,Array
对象还提供了很多非常实用的高阶函数。
every
every()
方法可以判断数组的所有元素是否满足测试条件。
例如,给定一个包含若干字符串的数组,判断所有字符串是否满足指定的测试条件:
find
find()
方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined
:
findIndex
findIndex()
和find()
类似,也是查找符合条件的第一个元素,不同之处在于findIndex()
会返回这个元素的索引,如果没有找到,返回-1
:
forEach
forEach()
和map()
类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。forEach()
常用于遍历数组,因此,传入的函数不需要返回值:
闭包
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023021250770016
函数作为返回值:高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
箭头函数
ES6 标准新增了一种新的函数:Arrow Function(箭头函数)。
为什么叫Arrow Function?因为它的定义用的就是一个箭头:
上面的箭头函数相当于:
在继续学习箭头函数之前,请测试你的浏览器是否支持 ES 6的 Arrow Function:
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,
- 一种像上面的,只包含一个表达式,连
{ ... }
和return
都省略掉了。 - 还有一种可以包含多条语句,这时候就不能省略
{ ... }
和return
:
如果参数不是一个,就需要用括号()
括起来:
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
因为和函数体的{ ... }
有语法冲突,所以要改为:
this
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this
是词法作用域,由上下文确定。
回顾前面的例子,由于JavaScript函数对this
绑定的错误处理,下面的例子无法得到预期结果:
现在,箭头函数完全修复了this
的指向,this
总是指向词法作用域,也就是外层调用者obj
:
如果使用箭头函数,以前的那种hack写法:
就不再需要了。
由于this
在箭头函数中已经按照词法作用域绑定了,所以,用call()
或者apply()
调用箭头函数时,无法对this
进行绑定,即传入的第一个参数被忽略:
generator
:https://www.liaoxuefeng.com/wiki/1022910821149312/1023024381818112
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。
ES6定义generator标准的哥们借鉴了Python的generator的概念和语法,如果你对Python的generator很熟悉,那么ES6的generator就是小菜一碟了。如果你对Python还不熟,赶快恶补Python教程!。
1.2 Javascript 定义 类(class)的几种方法
在面向对象编程中,类(class)是对象(object)的模板,定义了同一组对象(又称"实例")共有的属性和方法。类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。
早期的javascript
需求都很简单, 不支持面向对象,基本都是写成函数的,然后是面向过程的写法,后来慢慢的引入面向对象开发思想,再后来就慢慢写成 类
。
在 class 概念引入之前,js通过原型对象来实现类和类的继承,具体可以参考前文( 面向对象的 JavaScript:封装、继承与多态:https://zhuanlan.zhihu.com/p/112779427 )
在 ECMAScript 6 出现 class 的概念之后,才算是告别了直接通过原型对象来模拟类和类继承,但class 也只是基于 JavaScript 原型继承的语法糖,并没有引入新的对象继承模式,所以理解原型以及原型继承是非常重要的。通过 class 来创建对象,可以让代码更为简洁,复用性更高。
在js
中,写成类的本质基本都是 构造函数+原型
。下面,就讨论一下js类的几种写法:
构造函数
这是经典方法,也是教科书必教的方法。它用构造函数模拟 "类",在其内部用 this 关键字指代实例对象。
生成实例的时候,使用 new 关键字。
类的属性和方法,还可以定义在构造函数的 prototype 对象之上。
关于这种方法的详细介绍,查看系列文章《Javascript 面向对象编程》,这里就不多说了。它的主要缺点是,比较复杂,用到了 this 和 prototype,编写和阅读都很费力。
一:定义类并创建类的实例对象
在Javascript中,我们用function来定义类,如下:
你或许会说,疑?这个不是定义函数吗?没错,这个是定义函数,我们定义了一个 Shape 函数,并对x和y进行了初始化。不过,如果你换个角度来看,这个就是定义一个Shape类,里面有两个属性x和y,初始值分别是1和2,只不过,我们定义类的关键字是 function 而不是 class。然后,我们可以创建Shape类的对象aShape,如下:
二:定义 公有属性、私有属性
我们已经创建了aShape对象,但是,当我们试着访问它的属性时,会出错,如下:
这说明,用 var 定义的属性是私有的。我们需要使用 this 关键字来定义公有的属性
这样,我们就可以访问Shape的属性了,如:aShape.x = 2 ;
总结得到:用 var 可以定义类的private属性,而用 this 能定义类的 public 属性。
三:定义 公有方法、私有方法
在Javascript中,函数是 Function 类的实例,Function 间接继承自 Object,所以,函数也是一个对象,因此,我们可以用赋值的方法创建函数,当然,我们也可以将一个函数赋给类的一个属性变量,那么,这个属性变量就可以称为方法,因为它是一个可以执行的函数。代码如下:
我们在上面的代码中定义了一个 draw,并把一个 function 赋给它,下面,我们就可以通过 aShape 调用这个函数,OOP 中称为 公有方法,如:aShape.draw();
如果用 var 定义,那么这个 draw 就变成私有的了,OOP 中称为私有方法,如
这样就不能使用aShape.draw调用这个函数了。
三:构造函数
Javascript 并不支持 OOP,当然也就没有构造函数了,不过,我们可以自己模拟一个构造函数,让对象被创建时自动调用,代码如下:
在Shape的最后,我们人为的调用了init函数,那么,在创建了一个Shape对象是,init总会被自动调用,可以模拟我们的构造函数了。
四:带参数的构造函数
如何让构造函数带参数呢?其实很简单,将要传入的参数写入函数的参数列表中即可,如
这样,我们就可以这样创建对象:var aShape = new Shape( 0 , 1 );
五:静态属性、静态方法
在 Javascript 中如何定义静态的属性和方法呢?如下所示
有了静态属性和方法,我们就可以用类名来访问它了,如下
注意:静态属性和方法都是公有的,目前为止,我不知道如何让静态属性和方法变成私有的~
六:在方法中访问本类的公有属性和私有属性
在类的方法中访问自己的属性,Javascript对于公有属性和私有属性的访问方法有所不同,请大家看下面的代码
七:this 的注意事项
在 JavaScript 中,类中的 this 并不是一直指向我们的这个对象本身的,主要原因还是因为Javascript 并不是 OOP 语言,而且,函数 和 类 均用 function 定义,当然会引起一些小问题。
this 指针指错的场合一般在事件处理上面,我们想让某个对象的成员函数来响应某个事件,当事件被触发以后,系统会调用我们这个成员函数,但是,传入的 this 指针已经不是我们本身的对象了,当然,这时再在成员函数中调用this当然会出错了。
解决方法是我们在定义类的一开始就将this保存到一个私有的属性中,以后,我们可以用这个属性代替this。我用这个方法使用this指针相当安全,而且很是省心~
我们修改一下代码,解决this问题。对照第六部分的代码看,你一定就明白了
示例:
关于 Javascript 中的 OOP 实现就聊到这里,以上是最实用的内容,一般用 Javascript 定义类,创建对象用以上的代码已经足够了。当然,你还可以用 mootools 或 prototype 来定义类,创建对象。我用过mootools框架,感觉很不错,它对 Javascript 的类模拟就更完善了,还支持类的继承,有兴趣的读者可以去尝试一下。当然,如果使用了框架,那么在你的网页中就需要包含相关的js头文件,因此我还是希望读者能够在没有框架的情况下创建类,这样,代码效率较高,而且你也可以看到,要创建一个简单的类并不麻烦~
示例 1:
示例 2:
由上面控制台输出结果可知,p1和p2的确是类Person
的实例对象。instanceof
操作符左边是待检测类的对象,右边是定义类的构造函数。这里,instanceof
用来检测对象p1
是否属于Person
类。
这种方式的优点是:我们可以根据参数来构造不同的对象实例 ,缺点是每次构造实例对象时都会生成getName
方法,造成了内存的浪费 。
我们可以用一个外部函数来代替类方法,达到了每个对象共享同一个方法。改写后的类如下:
原型方式
原型方式:
- 缺点 就是不能通过参数来构造对象实例 (一般每个对象的属性是不相同的) ,
- 优点 是所有对象实例都共享getName方法(相对于构造函数方式),没有造成内存浪费 。
构造函数 + 原型方式
取前面两种的优点:
- a、用构造函数来定义类属性(字段)。
- b、用原型方式来定义类的方法。
这样,我们就既可以构造不同属性的对象,也可以让对象实例共享方法,不会造成内存的浪费。为了让js
代码风格更紧凑,我们让prototype
方法代码移到 function Person
的大括号内。
示例:
由 例1 和 例2 可以总结出javascript中定义类的步骤:
- 第一步:先定义一个构造函数,并设置初始化新对象的实例属性
- 第二步:给构造函数的prototype对象定义实例方法
- 第三步:给构造函数定义类字段和类属性
继承
新语法定义类,以及继承类
Object.create() 法
为了解决 "构造函数法" 的缺点,更方便地生成对象,Javascript的国际标准 ECMAScript 第五版(目前通行的是第三版),提出了一个新的方法 Object.create()。
用这个方法,"类" 就是一个 对象,不是 函数。
然后,直接用 Object.create() 生成实例,不需要用到 new。
目前,各大浏览器的最新版本(包括IE9)都部署了这个方法。如果遇到老式浏览器,可以用下面的代码自行部署。
这种方法比 "构造函数法" 简单,但是不能实现私有属性和私有方法,实例对象之间也不能共享数据,对"类"的模拟不够全面。
极简主义法 ( 推荐的方法
荷兰程序员 Gabor de Mooij提出了一种比 Object.create() 更好的新方法,他称这种方法为"极简主义法"(minimalist approach)。也是推荐的方法。
这种方法不使用 this 和 prototype,代码部署起来非常简单,这大概也是它被叫做 "极简主义法" 的原因。首先,它也是用一个对象模拟 "类"。在这个类里面,定义一个构造函数 createNew(),用来生成实例。
然后,在 createNew() 里面,定义一个实例对象,把这个实例对象作为返回值。
使用的时候,调用 createNew() 方法,就可以得到实例对象。
这种方法的好处是,容易理解,结构清晰优雅,符合传统的"面向对象编程"的构造,因此可以方便地部署下面的特性。
继承
让一个类继承另一个类,实现起来很方便。只要在前者的 createNew() 方法中,调用后者的createNew() 方法即可。
先定义一个 Animal 类。
然后,在 Cat 的 createNew() 方法中,调用 Animal 的 createNew() 方法。
这样得到的 Cat 实例,就会同时继承 Cat类 和 Animal类。
私有属性 和 私有方法
在 createNew() 方法中,只要不是定义在 cat 对象上的方法和属性,都是私有的。
上例的内部变量 sound,外部无法读取,只有通过 cat 的公有方法 makeSound() 来读取。
数据共享
有时候,我们需要所有实例对象,能够读写同一项内部数据。这个时候,只要把这个内部数据,封装在类对象的里面、createNew()方法的外面即可。
然后,生成两个实例对象:
这时,如果有一个实例对象,修改了共享的数据,另一个实例对象也会受到影响。
使用关键字 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中方式,类声明 和 类表达式:
ECMAScript 6 中定义一个类示例代码:
constructor 方法用来创建和初始化对象,而且一个类中有且只能有一个consctuctor方法,默认为constructor(){}。dog 就是Animal实例化的对象。
ES5 中创建类
在前面说到,class 只是基于现有的 JavaSript 实现类的语法糖,那先让我们简单使用 ES5 中的方法来模拟类,并实例化。
可以看出,class 中的 constructor() 方法就相当于 Animal() 构造函数,而在 class 中定义属性就相当于直接在原型对象上定义属性。我们不妨这样试一试:
可以看出 class 还是依靠原型对象来实现类的。
为什么说它是语法糖
因为类实际上它是一个 function,区别在于构造函数是函数作用域,类是块级作用域,类中的方法,都是定义在类的prototype上面,
类包含的属性和方法
类可以包含 构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。空的类定义照样有效。
实例属性必须定义在类方法中:
静态属性和原型属性避必须在类的外面定义:
实例属性顾名思义,就是对象独有的方法/属性,静态属性就是位于Class本身的属性,但是ES6中明确说明Class只有静态方法,没有静态属性,原型属性也很容易理解也很容易看出,就是位于原型链上的属性/方法。
类的构造函数
类的构造函数关键字是 constructor,它同等于原型中的 prototype.constructor。
如果没有写 constructor 函数,那么会默认有一个空的 constructor 函数。
当使用 new 操作符创建实例时,会调用 constructor 构造函数。
类的方法
类的静态方法
跟类的方法一样,只不过前面加上static关键字。
静态方法不会被实例继承。
父类的静态方法可以被子类继承。
所有在类中定义的方法会被实例继承,但是有时候我们并不想所有实例都能继承某个方法,这时候,static关键字就能达到你的目的,在声明方法前加上static关键字,这个方法就不会被实例继承,而是直接通过类来调用,它被叫做静态方法,如下:
静态方法虽然不能被实例继承,但是可以被子类继承,但是子类的实例依旧没有继承它:
类的私有方法
es6中没有提供这个方法,但是通常都是在方法前面加上下划线来表示。
取值函数(getter)和存值函数(setter)
在类中有 set 和 get 关键词,可以对某个属性设置存值和取值函数,拦截它的存取行为。
Class 的 继承
class 使用 extends 关键字来创建子类
但如果在子类中定义了 constructor 方法,必须先调用 super() 才能使用 this,因为子类并没有 this对象,而是继承父类的 this 对象,所以 super 必须在使用 this 关键字之前使用:
super 方法
注意如果子类如果没写constructor构造函数,则会默认有constructor构造函数和super方法,但是如果显性的写了constructor构造函数,那么必须在子类的构造函数中添加super方法,添加之后会调用父类的构造函数并得到父类的属性和方法,如果没有添加super方法则会报ReferenceError错误。
super 不仅可以调用父类的 constructor 函数,还可以调用父类上的方法:
示例:
super 方法中也可以传参
方法中的 this 指向
类的方法中如果有 this,那么它指向的是类的实例。但是如果将它单独拿出来使用那么会报错。
解决办法有2种:
- 在构造函数中绑定 this
- 使用箭头函数
区分是否继承了这个类
区分是否继承了这个类使用Object.getPrototypeOf函数。
1.3 详解 Javascript 中的 Object 对象
From:https://www.jb51.net/article/80177.htm
JS 中的 所有对象 都是 继承自 Object对象
创建 对象
"对象" 是一组相似数据和功能的集合,用它可以来模拟现实世界中的任何东西。
在 Javascript 中,创建对象的方式通常有两种方式:
- 构造函数。这种方式使用 new 关键字,接着跟上 Object 构造函数,再来给对象实例动态添加上不同的属性。这种方式相对来说比较繁琐,一般推荐使用对象字面量来创建对象。
- 对象字面量。对象字面量很好理解,使用 key/value 的形式直接创建对象,通过花括号将对象的属性包起来,对象的每个属性之间用逗号隔开。注意:如果是最后一个属性,后面就不要加逗号,因为在一些旧的浏览器下会报错。
注意:
构造函数 和 对象字面量 这两种方法有一个缺点就是:如果要创建多个对象,写起来很繁琐,所以后来就有了一种创建自定义构造函数的方法来创建对象,如下所示:
这种方式可以很方便的创建多个同样的对象,也是目前比较常用的方法。
javascript 中 function(){}(), new function(), new Function(), Function
javascript 中的类的构造:
javascript 中有对象的概念,却没有类的概念。
- 类 是一种抽象的概念,例如:动物、植物;
- 对象 则是指这种概念中的实体,比如 "中国人、美国人、杨树、柳树";
- 实例化
类所拥有的特征,其实例化对象,也一定拥有这些特征,而且实例化后可能拥有更多特征。
javascript 在用到对象时,完全没有类的概念,但是编程的世界里,无奇不有,可以通过 function 构造出一种假想的类,从而实现 javascript 中类的构造。比如,我们通过下面的方法来构造一个类:
function(){}()
在《javascript立即执行某个函数:插件中function(){}()再思考》一文中,我详细阐述了 function(){}() 的作用及理解思路。这里不再赘述,现在,我们面临的新问题是,知道了它的作用,我们如何使用它?让我们来看一段代码:
当我们要使用一个变量时,我们希望这个变量在一个环节完成我们的赋值,使用上面的这种方法,可以减少代码上下文执行逻辑,如果按照我们以前的方法,代码可能会写成:
看上去好像比上面的操作简洁多了,只需要两行代码。但是我们仔细去观察,就会发现第一段代码其实本身仅是一个赋值操作,在 function 中完成的所有动作将会在function执行完后全部释放,整个代码看上去好像只执行了一条语句一样。
而实际上更重要的意义在于它可以让一个变量在初始化时,就具备了运算结果的效果。
使用 new function
上面讲了 javascript 中的类,而使用 new function 可以实例化这个类。但是我们实际上有的时候在为一个变量赋值的时候,希望直接将它初始化为一个可操作的对象,比如像这样:
当我们要对数据库 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 属性。实际上确实是这样
在原型的基础上通过prototype新增属性或方法,则以该对象为原型的实例化对象中,必然存在新增的属性或方法,而且它的内容是静态不可重载的。原型之所以被称为原型,可能正是因为这种不可重载的特质。
比如上面的这段代码,会导致每一个实例化的function,都会具备一个show方法。而如果我们自己创建了一个类,则可以通过 prototype 将之转化为原型:
这时,对于 cat1 而言,Cat 就是原型,而该原型拥有一个 run 的原始方法,所以无论实例化多少个 Cat,每一个实例化对象都有 run 方法,而且该方法是不能被重载的,通过 cat1.run = function(){} 是无效的。
为了和其他语言的类的定义方法统一,我们可以将这种原型属性在定义类的时候,写在类的构造里面:
new Function()
在理解了 Function 原型的概念之后,再来看 new Function()就显得很容易了。首先来看下我们是怎么使用这种奇特的写法的:
new Function(参数1,参数2,…,参数n,函数体),它的本意其实是通过实例化一个Function原型,得到一个数据类型为function的对象,也就是一个函数,而该变量就是函数名。
this 在这类 function 中的指向
this 在 javascript 中真的是无法让我们捉摸透彻。但是有一个小窍门,就是:一般情况下,this 指向的是当前实例化对象,如果没有找到该对象,则是指向 window。从使用上来讲,我们应该排除new Function 的讨论,因为它和我们常用的函数声明是一致的。
- 普通的函数中this的指向:函数声明的时候,如果使用了this,那么就要看是把该函数当做一个对象加以返回,还是以仅执行函数体。普通函数执行时,我们完全没有引入对象、类这些概念,因此,this 指向 window。通过代码来看下:
首先是声明一个函数message,在函数中this.msg实际上就是window.msg,也实际上就是代码开头的msg。因此,当执行完message(‘ok’)的时候,开头的全局变量msg也被赋值为ok。
- 通过 function 构造类时 this 的指向:如果function被构造为一个类,那么必然存在该类被实例化的一个过程,如果没有实例化,那么该类实际上并没有在程序中被使用。而一旦实例化,那么this将指向实例化的对象。
上面代码中标记了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 方法。
hasOwnProperty(propertyName) 方法
hasOwnProperty 方法接收一个字符串参数,该参数表示属性名称,用来判断该属性是否在当前对象实例中,而不是在对象的原型链中。我们来看看下面这个例子:
在这个例子中,首先定义了一个数组对象的 实例arr,我们知道,数组对象实际是通过 原型链 ( 下面会介绍 ) 继承了Object 对象,然后拥有自己的一些属性,可以通过 hasOwnProperty 方法 判断 length 是 arr 自己的属性,然而 hasOwnProperty方法 是在 原型链 上的属性。
hasOwnProperty 方法 可以和 for..in 结合起来获取 对象自己的 key
isPrototypeOf(Object) 方法
isPrototype 方法接收一个对象,用来判断当前对象是否在传入的参数对象的原型链上,说起来有点抽象,我们来看看代码。
上面代码中 MyObject 是继承自 Object 对象的,而在JS中,继承是通过 prototype 来实现的,所以 Object 的 prototype 必定在 MyObject 对象实例的原型链上。
propertyIsEnumerable(prototypeName) 方法
prototypeIsEnumerable 用来判断给定的属性是否可以被 for..in 语句给枚举出来。看下面代码:
执行这段代码输出字符串 “name”,这就说明通过 for…in 语句可以得到 obj 的 name 这个属性,但是我们知道,obj 的属性还有很多,比如 constructor,比如hasOwnPrototype 等等,但是它们没有被输出,说明这些属性不能被 for…in 给枚举出来,可以通过 propertyIsEnumerable 方法来得到。
判断 “constructor” 是否可以被枚举,输出 false 说明无法被枚举出来。
toLocaleString()
toLocalString 方法返回对象的字符串表示,和代码的执行环境有关。
toString()
toString 用来返回对象的字符串表示。
valueOf()
valueOf 方法返回对象的原始值,可能是字符串、数值 或 bool值 等,看具体的对象。
如代码所示,三个不同的对象实例调用 valueOf 返回不同的数据。
属性 的 类型
在 Javascript 中,属性有两种类型,分别是
- 数据属性
- 访问器属性
我们来看看这两种属性具体是什么东西。
数据
数据属性:可以理解为我们平时定义对象时赋予的属性,它可以进行读和写。但是,ES5中定义了一些 特性,这些特性是用来描述属性的各种特征。即 特性的作用 是描述 属性的各种特征。特性是内部值,不能直接访问到。特性通过用两对方括号表示,比如[[Enumerable]]。
属性的特性会有一些默认值,要修改特性的默认值,必须使用 ES5 定义的新方法 Object.defineProperty 方法来修改
数据属性有4个描述其特征的特性,下面将依次说明每一个特性:
(1)[[Configurable]]:该特性表示是否可以通过 delete 操作符来删除属性,默认值是 true。
这段代码很明显,通过 delete 删除了 obj 的 name 属性后,我们再访问 name 属性就访问不到了。
我们通过 Object.defineProperty 方法来修改 [[Configurable]] 特性。
通过将 configurable 特性设置成 false 之后,delete 就无法删除 name 属性了,如果在严格模式下,使用 delete 去删除就会报错。
(2)[[Enumerable]]:表示是否能够通过 for…in 语句来枚举出属性,默认是 true
我们来看看前面的例子:
这段代码只输出了 name 属性,我们来将 constructor 属性的 [[Enumerable]] 设置为 true 试试。
这段代码中,for…in 循环得到了 name 和 constructor 两个属性,而通过 propertyIsEnumerable 方法来判断 constructor 也返回了true。
(3)[[Writable]]:表示属性值是否可以修改,默认为true。如果 [[Writable]] 被设置成 false,尝试修改时将没有效果,在严格模式下会报错
(4)[[Value]]:表示属性的值,默认为 undefined
我们通过一个简单的例子来看看这两个特性:
我们首先定义了 obj 对象的 name 属性值为 “name”,然后通过 defineProperty 方法来修改值,并且将其设置为不可修改的。接着我们再修改 name 属性的值,可以发现修改无效。
如果我们通过 defineProperty 来修改 name 属性的值,是否可以修改呢?答案是可以的:
访问器
访问器属性有点类似于 C# 中的属性,和数据属性的区别在于,它没有数据属性的 [[Writable]] 和 [[Value]] 两个特性,而是拥有一对 getter 和 setter 函数。
- [[Get]]:读取属性时调用的函数,默认是 undefined
- [[Set]]:设置属性时调用的函数,默认是 undefined
getter 和 setter 是一个很有用的东西,假设有两个属性,其中第二个属性值会随着第一个属性值的变化而变化。这种场景在我们平时的编码中起始是非常常见的。在之前的做法中,我们往往要去手动修改第二个属性的值,那现在我们就可以通过 get 和 set 函数来解决这个问题。看下面这个例子:
通过修改 age 的值,type 的值也会相应的修改,这样我们就不用再手动的去修改 type 的值了。
下面这种方式也是可以实现同样的效果:
访问器属性 的 注意点
关于访问器属性,有几点要注意:
- 1、严格模式下,必须同时设置 get 和 set
- 2、非严格模式下,可以只设置其中一个,如果只设置 get,则属性是只读的,如果只设置 set,属性则无法读取
- 3、Object.defineProperty 是 ES5 中的新方法,IE9(IE8部分实现,只有dom对象才支持)以下浏览器不支持,一些旧的浏览器可以通过非标准方法defineGetter()和defineSetter()来设置,这里就不说明了,有兴趣的同学可以查找相关资料。
特性
ES5 提供了一些读取或操作属性特性的方法,前面用到的 Object.defineProperty 就是其中之一。我总结了一些比较常用的方法如下:
(1)Object.defineProperty
定义一个对象的属性,这个方法前面我们已经用到多次,简单说说其用法。
defineProperty 有点类似于定于在 Object 上的静态方法,通过 Object 直接调用,它接收3个参数:
- obj:需要定义属性的对象
- propNane:需要被定义的属性名称
- defineProperty:属性描述符,包含一些属性的特性定义
例子如下:
(2)Object.defineProperties
和 defineProperty 类似,是用来定义对象属性的,不同的是它可以用来同时定义多个属性,我们通过命名也可以看出来,用法如下:
(3)Object.getOwnPropertyDescriptor
ES5 中还提供了一个读取特性值的方法,该方法接收对象及其属性名作为两个参数,返回一个对象,根据属性类型的不同,返回对象会包含不同的值。
Object 的 方法
在 ES5 中,Object 对象上新增了一批方法,这些方法可以直接通过 Object 进行访问,前面用到的 defineProperty 就是新增的方法之一。除此之外还有很多方法,我将其总结归纳如下:
对象创建型方法 Object.create(proto, [propertiesObject])
在前面我们提到,创建一个对象有两种方法:构造函数 和 对象字面量。
这两种方法有一个缺点就是:如果要创建多个对象,写起来很繁琐,所以后来就有了一种创建自定义构造函数的方法来创建对象,如下所示:
这种方式可以很方便的创建多个同样的对象,也是目前比较常用的方法。
ES5 提供的 Object.create 方法也是一个创建对象的方法,这个方法允许为创建的对象选择原型对象,不需要定义一个构造函数。用法如下:
这个方法接收的第一个参数作为被创建对象的原型,第二个参数是对象的属性。
注意:在这个例子中,name属性是无法被修改的,因为它没有设置writable特性,默认则为false。
个人看法:Object.create这种创建对象的方式略显繁琐,除非是需要修改属性的特性,否则不建议使用这种方式创建对象。
获取 属性
Object.keys 获取自身属性,但不包括原型中的属性
Object.keys 是 es5 中新增的方法,用来获取对象自身所有的可枚举的属性名,但不包括原型中的属性,然后返回一个由属性名组成的数组。
注意它同 for..in 一样不能保证属性按对象原来的顺序输出。
代码中返回了 firstName,并没有返回从 prototype 继承而来的 lastName 和 不可枚举的相关属性。
在一些旧的浏览器中,我们可以使用 hasOwnProperty 和 for…in
getOwnPropertyNames 获取自身 可枚举 和 不可枚举 的 属性
Object.getOwnPropertyNames 也是 es5 中新增的方法,返回对象自身的所有属性的属性名(包括可枚举和不可枚举的所有属性)组成的数组,但不会获取原型链上的属性。
我们定义给 son 对象定义了一个不可枚举的属性 age,然后通过 keys 和 getOwnPropertyNames 两个方法来获取属性列表,能明显看出了两者区别。
属性 特性型 方法
这个主要是前面提到的三个方法:
- defineProperty,
- defineProperties
- getOwnPropertyDescriptor
对象 限制型
ES5 中提供了一系列限制对象被修改的方法,用来防止被某些对象被无意间修改导致的错误。每种限制类型包含一个判断方法和一个设置方法。
阻止对象扩展
Object.preventExtensions()
该方法接收一个要被设置成无法扩展的对象作为参数,需要注意两点:
- 1、对象的属性不可用扩展,但是已存在的属性可以被删除
- 2、无法添加新属性指的是无法在自身上添加属性,如果是在对象的原型上,还是可以添加属性的。
Object.isExtensible 方法用来判断一个对象是否可扩展,默认情况是 true
将对象密封
Object.seal 可以密封一个对象并返回被密封的对象。
密封对象无法添加或删除已有属性,也无法修改属性的 enumerable,writable,configurable,但是可以修改属性值。
将对象密封后,使用 delete 删除对象属性,还是可以访问得到属性。
通过 Object.isSealed 可以用来判断一个对象是否被密封了。
冻结对象
Object.freeze 方法用来冻结一个对象,被冻结的对象将无法添加,修改,删除属性值,也无法修改属性的特性值,即这个对象无法被修改。
分析上面的代码我们可以发现,被冻结的对象无法删除自身的属性,但是通过其原型对象还是可以新增属性的。
通过Object.isFrozen可以用来判断一个对象是否被冻结了。
可以发现:这三个限制对象的方法的限制程度是依次上升的。
Javascript 的 Object
参考:Javascript Object常用方法总结 - fozero - 博客园
Object.keys(ojb)
Object.keys(obj) 方法是 JavaScript 中用于遍历对象属性的一个方法 。它传入的参数是一个对象,返回的是一个数组,数组中包含的是该对象所有的属性名。如:
这里有一道关于 Object.keys 的题目:输出对象中值大于 2的 key 的数组
Object.keys 是 es5 中新增的方法,用来获取对象自身所有的可枚举的属性名,但不包括原型中的属性,然后返回一个由属性名组成的数组。注意它同 for..in 一样不能保证属性按对象原来的顺序输出。
Object.getOwnPropertyNames 也是 es5 中新增的方法,返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性。
Array.filter(function)
对数组进行过滤返回符合条件的数组。
Object.values()
Object.values
返回数组的成员顺序,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a。Object.values 只返回对象自身的可遍历属性。
如果 Object.values 方法的参数是一个字符串,会返回各个字符组成的一个数组。
上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组。
如果参数不是对象,Object.values 会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values 会返回空数组。
Object.create()
Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__
语法:Object.create(proto, [propertiesObject])
参数
:proto 新创建对象的原型对象。
:propertiesObject 可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。
返回值:一个新对象,带着指定的原型对象和属性。如:
Object.create()
以上两种方式有什么区别?
1 的缺点:
执行了 new,相当于运行了一遍 A ,如果在 A 里做了一些其它事情(如改变全局变量)就会有副作用。
用 A 创建的对象做原型,里面可能会有一些冗余的属性。
2 模拟了 new 的执行过程
Object.hasOwnProperty()
判断对象自身属性中是否具有指定的属性。这个方法是不包括对象原型链上的方法的。
判断某个对象是否拥有某个属性,判断的方法有很多,常用的方法就是 object.hasOwnProperty('×××')
以上,obj 对象存在的 name 属性的时候,调用这个方法才是返回 true,我们知道其实每个对象实例的原型链上存在 toString 方法,在这里打印 false,说明这个方法只是表明实例对象的属性,不包括原型链上的属性。
Object.getOwnPropertyNames()
Object.getOwnPropertyNames() 方法返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性。
Object.getOwnPropertyNames 和 Object.keys
Object.getOwnPropertyNames 和 Object.keys
- Object.keys 只适用于可枚举的属性,
- Object.getOwnPropertyNames 返回对象自动的全部属性名称。
es6 中 JavaScript 对象方法 Object.assign()
Object.assign
1、如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
2、如果只有一个参数,Object.assign 会直接返回该参数。
3、如果该参数不是对象,则会先转成对象,然后返回。
4、由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
5、Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
上面代码中,源对象obj1的a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
常见用途
( 1 )为对象添加属性
上面方法通过Object.assign方法,将x属性和y属性添加到Point类的对象实例。
( 2 )为对象添加方法
上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用 assign 方法添加到 SomeClass.prototype 之中。
( 3 )克隆对象
上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。
( 4 )合并多个对象
将多个对象合并到某个对象。
如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。
( 5 )为属性 指定 默认值
DEFAULTS 对象是默认值,options 对象是用户提供的参数。Object.assign 方法将 DEFAULTS 和 options 合并成一个新对象,如果两者有同名属性,则 option 的属性值会覆盖 DEFAULTS 的属性值。注意,由于存在深拷贝的问题,DEFAULTS 对象 和 options对象的所有属性的值,都只能是简单类型,而不能指向另一个对象。否则,将导致DEFAULTS 对象的该属性不起作用。
Object.defineProperty()
Object.defineProperty
使用构造函数定义对象和属性
语法:Object.defineProperty(obj, prop, descriptor)
参数说明
- obj:必需。目标对象
- prop:必需。需定义或修改的属性的名字
- descriptor:必需。目标属性所拥有的特性
给对象的属性添加特性描述,目前提供两种形式:
- 数据描述
- 存取器描述
数据 描述
修改或定义对象的某个属性的时候,给这个属性添加一些特性, 数据描述中的属性都是可选的
- value: 设置属性的值
- writable: 值是否可以重写。true | false
- enumerable: 目标属性是否可以被枚举。true | false
- configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false
存取器 描述
使用存取器描述属性的特性的时候,允许设置以下特性属性, 当使用了getter 或 setter 方法,不允许使用 writable 和 value 这两个属性
getter / setter
getter 是一种获得属性值的方法。setter是一种设置属性值的方法。使用 get/set 属性来定义对应的方法
原型链
- 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__
- 1. JS 代码还没运行的时候,JS 环境里已经有一个 window 对象了。
- 2. window 对象有一个 Object 属性,window.Object 是一个 函数对象
- 3. window.Object 这个函数对象有一个重要属性是 prototype
- 4. window.Object.prototype 里面有一堆属性
- 5. 所有的实例函数的 __proto__ 都会指向 构造函数的 prototype
- 6. constructor 是 反向的prototype
上面定义了一个 空对象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__ 组成的链子一直走下去的,这个过程就叫做 原型链
上面是一个 "链",下面继续深入,看下数组
再复杂一下,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。
示例:
person1 和 person2 都是 Person 的实例。这两个实例都有一个 constructor (构造函数)属性,该属性(是一个指针)指向 Person。 即:
2.构造函数的执行过程
- (1) 当以 new 关键字调用时,会创建一个新的内存空间,标记为 Person 的实例
- (2) 函数体内部的 this 指向该内存,每当创建一个实例的时候,就会创建一个新的内存空间
- (3) 给 this 添加属性,就相当于给实例添加属性
- (4) 由于函数体内部的 this 指向新创建的内存空间,默认返回 this ,就相当于默认返回了该内存空间
prototype
每个 函数 都有一个 prototype 属性,就是我们经常在各种例子中看到的那个 prototype ,指向调用该构造函数而创建的 实例的原型,
比如:上面例子中的 person1 和 person2 的原型
示例:
那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?
其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数 而创建 的 实例的原型,也就是这个例子中的 person1 和 person2 的原型。
那什么是原型呢 ?
可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
让我们用一张图表示构造函数和实例原型之间的关系:
在这张图中我们用 Object.prototype 表示实例原型。
那么我们该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性:__proto__
JS 的继承是用过原型链实现的
给构造函数添加属性
prototype 和 __proto__ 区别 :
- 函数(Function)才有 prototype 属性,
- 对象(除Object)拥有__proto__。
js 原型链 prototype __proto
__proto__ 指向
__proto__
这是每一个 JavaScript 对象 (除了 null ) 都具有的一个属性,叫 __proto__,这个属性会指向该对象的原型。
为了证明这一点,我们可以在火狐或者谷歌中输入:
于是我们更新下关系图:
既然 实例对象 和 构造函数 都可以指向原型,那么 原型 是否有属性指向 构造函数或者实例 呢?
constructor
- 原型 指向 实例 倒是没有,因为一个构造函数可以生成多个实例。
- 但是 原型 指向 构造函数 倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。
为了验证这一点,我们可以尝试:
所以再更新下关系图:
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)
综上我们已经得出:
首先要清楚:JS中所有事物都是对象,对象是拥有属性和方法的数据。所以函数也是对象
当创建一个函数的时候,发生了什么?
实际上,
- 函数是 Function类型的实例,此时可以把每一个创建出来的函数,当成是Function类型的实例对象,
- 所以函数本身拥有的对象属性,来源于Function,即 Fn.Constructor 就是 Function
- 但是与此同时要注意:Function.prototype.__proto__ === Object.prototype 可以理解为:构造器函数的构造函数是Object 也可以简单的理解:函数即对象
了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲 实例 和 原型 的关系:
定义对象的两种方式:
原型链中的继承
实例 与 原型
当读取 实例的属性 时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
在这个例子中,我们给实例对象 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 属性,我们看个例子:
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到 constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
__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. 先来看个最简单的自执行函数
相当于声明并调用
2. 自执行函数也可以有名字
3. 执行函数也可以传参
总结:自执行函数 在调用上与普通函数一样,可以匿名,可以传参。只不过是在声明的时候自调用了一次
常见的自执行匿名函数:
- 第一种:(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
- 第五种:void function(){ alert('hellow world!'); }(); // 默认返回值 是 undefine。 void function(x) {x = x-1;}(9);
- 第六种:+function(){ alert('hellow world!'); }(); // 默认返回值 是 NaN
- 第七种:-function(){ alert('hellow world!'); }(); // 默认返回值 是 NaN
- 第八种:!function(){ alert('hellow world!'); }(); // 默认返回 true,是一个 bool 值
- 第九种:( function(x,y){return x+y;}(3,4) ); // 一个() ,里面写 function(){}() 默认返回值 是 undefine
- 匿名函数的执行放在 [ ] 中:
- 匿名函数前加 typeof
示例:匿名函数自执行 实现 异步函数递归
自执行匿名函数 执行耗时结果:
- new 方法永远最慢。
- 括号 在测试里表现始终很快,在大多数情况下比感叹号更快,甚至可以说是最优的。
- 加减号 在chrome表现惊人,而且在其他浏览器下也普遍很快,相比加号效果更好。当然这只是个简单测试,不能说明问题。
但有些结论是有意义的:括号和加减号最优
面向对象 --- 封装
封装:把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口。
在ES6之前,是不存在 class 这个语法糖类的。所以实现大多采用原型对象和构造函数
- 私有 属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var声明的属性)
- 公有 属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
- 静态 属性和方法:定义在构造函数上的方法(比如Cat.xxx),不需要实例就可以调用(例如Object.assign())
在ES6之后,存在class这个语法糖类。当你使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回,因此它被称为constructor构造方法(函数)。
私有变量、函数
在函数内部定义的变量和函数如果不对外提供接口,那么外部将无法访问到,也就是变为私有变量和私有函数。
静态变量、函数
当定义一个函数后通过 “.”为其添加的属性和函数,通过对象本身仍然可以访问得到,但是其实例却访问不到,这样的变量和函数分别被称为静态变量和静态函数。
实例变量、函数
在面向对象编程中除了一些库函数我们还是希望在对象定义的时候同时定义一些属性和方法,实例化后可以访问,JavaScript也能做到这样。
测试
请写出以下输出结果:
解读:首先定义了一个叫 Foo 的函数,之后为 Foo 创建了一个叫 getName 的静态属性存储了一个匿名函数,之后为 Foo 的原型对象新创建了一个叫 getName 的匿名函数。之后又通过函数变量表达式创建了一个 getName 的函数,最后再声明一个叫 getName 函数。
先来剧透一下答案,再来看看具体分析
- 第一问:Foo.getName 自然是访问 Foo 函数上存储的静态属性,自然是 2
- 第二问:直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫 getName 的函数,所以跟 1 2 3 都没什么关系。但是此处有两个坑,一是变量声明提升,二是函数表达式。关于函数变量提示,此处省略一万字。。。。题中代码最终执行时的是
- 第三问: 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 对象。
使用 let 、const 声明的 变量,不会绑定到 window
修改代码:
foo 函数属于 window,inner 是 foo 的内嵌函数,所以 this 指向 window
隐式绑定
示例代码:
隐式丢失:就是被隐式绑定的函数,在特定的情况下会丢失绑定对象。
特定情况是指:
- 1. 使用另一个变量来给函数取别名
- 2. 或者 将函数作为参数传递时会被隐式赋值,回调函数丢失 this 绑定
示例:
示例:
示例:
结论:如果把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的 this 指向无关。在非严格模式下,会把该函数的 this 绑定到 window 上。严格模式下绑定到 undefine
示例:
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 ):
示例:
new 绑定
new 和 call 同时出现
箭头函数
箭头函数绑定中:this 的指向由外层作用域决定的,并且指向函数定义时的 this,而不是执行时的 this。
示例:
示例:( 箭头函数 与 call 结合 )
箭头函数里面的 this 是由外层作用域来决定的,并且指向函数定义时的 this,而不是执行时的 this
字面量创建的对象,作用域是 window,如果里面有箭头函数属性的话, this 指向的是 window
构造函数创建的对象,作用域是可以理解为是这个构造函数,且这个构造函数的 this 是指向新建的对象的,因此 this 指向这个对象
箭头函数的 this 是无法通过 bind、call、apply 来直接修改,但是可以用过改变作用域中 this 的指向来间接修改
new 的过程中到底发生了什么?
1. 新生成了一个对象
2. 链接到原型
3. 绑定 this
4. 返回新对象
更多可以参看 《JavaScript高级程序设计 第4版》
js 逆向技巧
From:js逆向技巧 - 走看看
一、总结
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钩子
二、js逆向技巧
博客对应课程的视频位置:
当我们抓取网页端数据时,经常被加密参数、加密数据所困扰,如何快速定位这些加解密函数,尤为重要。本片文章是我逆向js时一些技巧的总结,如有遗漏,欢迎补充。
所需环境:Chrome浏览器
1. 搜索
1.1 全局搜索 ( Ctrl + shift + f )
适用于根据关键词快速定位关键文件及代码
当前页面 右键 ---> 检查,弹出检查工具
搜索支持 关键词、正则表达式
1.2 代码内搜索 ( Ctrl + f )
适用于根据关键词快速定位关键代码
点击代码,然后按 ctrl+f 或 command+f 调出搜索框。搜索支持 关键词、css表达式、xpath
2. debug
2.1 常规 debug
适用于分析关键函数代码逻辑
a、埋下断点
b、调试
如图所示,标记了 1 到 6,下面分别介绍其含义
- 1. 执行到下一个端点
- 2. 执行下一步,不会进入所调用的函数内部
- 3. 进入所调用的函数内部
- 4. 跳出函数内部
- 5. 一步步执行代码,遇到有函数调用,则进入函数
- 6.Call Stack 为代码调用的堆栈信息,代码执行顺序为由下至上,这对于着关键函数前后调用关系很有帮助
2.2 XHR debug
匹配url中关键词,匹配到则跳转到参数生成处,适用于url中的加密参数全局搜索搜不到,可采用这种方式拦截
2.3 行为 debug
适用于点击按钮时,分析代码执行逻辑
如图所示,可快速定位点击探索按钮后,所执行的js。
3 查看请求调用的堆栈
可以在 Network 选项卡下,该请求的 Initiator 列里看到它的调用栈,调用顺序由上而下:
4. 执行堆内存中的函数
当 debug 到某一个函数时,我们想主动调用,比如传递下自定义的参数,这时可以在检查工具里的 console 里调用
此处要注意,只有debug打这个函数时,控制台里才可以调用。如果想保留这个函数,可使用this.xxx=xxx 的方式。之后调用时无需debug到xxx函数,直接使用this.xxx 即可。
5. 修改堆栈中的参数值
6. 写 js 代码
7. 打印 windows 对象的值
在 console 中输入如下代码,如只打印 _$ 开头的变量值
8. 勾子
以 chrome 插件的方式,在匹配到关键词处插入断点
8.1 cookie 钩子
用于定位 cookie 中关键参数生成位置
当 cookie 中匹配到了 TSdc75a61a
, 则插入断点。
8.2 请求钩子
用于定位请求中关键参数生成位置
当请求的 url 里包含 MmEwMD
时,则插入断点
8.3 header 钩子
用于定位 header 中关键参数生成位置
当 header 中包含 Authorization
时,则插入断点
8.4 manifest.json
插件的配置文件
使用方法
a、如图所示,创建一个文件夹,文件夹中创建一个钩子函数文件inject.js 及 插件的配置文件 mainfest.json 即可
b、打开chrome 的扩展程序, 加载已解压的扩展程序,选择步骤1创建的文件夹即可
c、切换回原网页,刷新页面,若钩子函数关键词匹配到了,则触发debug
刷新型 cookie 反爬
瑞数、加固乐
调试干扰
无限 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 函数
- 3. 如果是定时器,并且 2失效了,可以重构定时器
4. 在以上方式都失效时,向上找堆栈,在进入无限debugger之前打上断点将触发无限debugger 的函数置空(最麻烦,但是适用性最广)
3. 无限 debugger 在加密逻辑之后:不用管,script/ 第一行断点打上,从头开始
控制台 检测
:https://match.yuanrenxue.com/match/16
打个 script 断点,当跳转到主页时,查看 源码,代码如下:
上面过控制台检测
打上断点之后,当要运行跳转时,直接把跳转 置为 空函数 即可
控制台检测原理:原理就是使用 console 的特性,只有在控制台打开的时候,console 才会对一些信息和内容进行打印。如果设置一个定时器,在定时器中不断循环获取一个参数x,并且对这个参数x进行 hook,利用 Object.defineProperty 处理其get 属性,那么当打开控制台的一瞬间,console 就会生效,获取属性并触发 hook,执行 Object.defineProperty 内的逻辑。
chrome 浏览器有这个特性, firefox 没有这个特性,可以多换几个浏览器试一下。
内存爆破原理
其实可以叫做 "蜜罐内存爆破",当检测出来时( 例如:js 代码进行了格式化,导致特征字符穿不匹配,使用 js 正则检测出来 ),走到蜜罐里面一直循环。
对于 js 格式化被正则检测出来,可以分段进行 js 格式化,逐渐排出,找到关键位置,然后让正则检测时返回 true
内存爆破,指 js 通过死循环/频繁操作数据库( 包括cookie ) / 频繁调取 history 等方式,使浏览器崩溃的一种反调试手段。
关键词:频繁
还有一种特性情况:js文件很大,电脑内存不足(这种情况在调试层面几乎无解)
特点:
1. 正常运行时,一切正常
2. 调试时利用时间差,调试特点等讲控制流推入循环
3. ob混淆内存爆破原理:利用正则/toString() 判断代码是否进行格式化
4. 利用浏览器指纹判断是否为浏览器环境